diff --git a/Files/BackOne/addons/comms-core/addon.json b/Files/BackOne/addons/comms-core/addon.json
new file mode 100644
index 0000000..dae6899
--- /dev/null
+++ b/Files/BackOne/addons/comms-core/addon.json
@@ -0,0 +1,10 @@
+{
+ "id": "comms-core",
+ "name": "Comms Core",
+ "version": "1.17.1",
+ "requires": [],
+ "backend": {
+ "routes": ["/controller/comms"],
+ "websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
+ }
+}
diff --git a/Makefile b/Makefile
index 1bedf30..c9dec26 100644
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,10 @@ ifeq ($(OSTYPE),NetBSD)
include make-netbsd.mk
endif
+ifeq ($(OS),Windows_NT)
+ include make-windows.mk
+endif
+
drone:
@echo "rendering .drone.yaml from .drone.jsonnet"
drone jsonnet --format --stream
diff --git a/OpenType-TT/BackOneLogo-Regular-SVG.ttf b/OpenType-TT/BackOneLogo-Regular-SVG.ttf
new file mode 100755
index 0000000..68a070e
Binary files /dev/null and b/OpenType-TT/BackOneLogo-Regular-SVG.ttf differ
diff --git a/OpenType-TT/BackOneLogo-Regular.ttf b/OpenType-TT/BackOneLogo-Regular.ttf
new file mode 100755
index 0000000..e2c1b66
Binary files /dev/null and b/OpenType-TT/BackOneLogo-Regular.ttf differ
diff --git a/Support/BackOne/app/addon-loader.js b/Support/BackOne/app/addon-loader.js
new file mode 100644
index 0000000..5fffc36
--- /dev/null
+++ b/Support/BackOne/app/addon-loader.js
@@ -0,0 +1,57 @@
+window.BackOneRingingPopup = {
+ _audio: null,
+ showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
+ const el = document.getElementById('ringing-popup');
+ if (!el) return;
+ el.classList.remove('ringing-hidden');
+ el.innerHTML = `
+
+
Incoming ${callType} call
+
${callerName || 'Unknown peer'}
+
Accept
+
Reject
+
`;
+ el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
+ el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
+ this.playRingtone();
+ if (Notification.permission === 'default') Notification.requestPermission();
+ if (Notification.permission === 'granted') {
+ new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
+ }
+ },
+ dismiss() {
+ const el = document.getElementById('ringing-popup');
+ if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
+ this.stopRingtone();
+ },
+ playRingtone() {
+ this.stopRingtone();
+ try {
+ this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
+ this._audio.loop = true;
+ this._audio.play().catch(() => {});
+ } catch (_) {}
+ },
+ stopRingtone() {
+ if (this._audio) { this._audio.pause(); this._audio = null; }
+ }
+};
+
+async function loadAddonManifest() {
+ const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
+ if (!res.ok) {
+ console.warn('manifest.json fetch failed:', res.status);
+ return [];
+ }
+ const data = await res.json();
+ return Array.isArray(data) ? data : [];
+}
+
+async function loadAddonModule(entry) {
+ const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
+ await import(entryPath);
+ const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
+ return factory ? factory() : null;
+}
+
+window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
diff --git a/Support/BackOne/app/auth-store.js b/Support/BackOne/app/auth-store.js
new file mode 100644
index 0000000..533f850
--- /dev/null
+++ b/Support/BackOne/app/auth-store.js
@@ -0,0 +1,195 @@
+/**
+ * BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
+ */
+window.BackOneAuth = (function () {
+ const LS_KEY = 'backone.authToken';
+ let cache = null;
+
+ function trimToken(s) {
+ return String(s || '').replace(/\s+/g, '');
+ }
+
+ function tokenFromUrl() {
+ try {
+ const params = new URLSearchParams(location.search);
+ const fromQuery = trimToken(params.get('auth') || params.get('token'));
+ if (fromQuery) return fromQuery;
+ const hash = (location.hash || '').replace(/^#\/?/, '');
+ const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
+ return trimToken(hashParams.get('auth') || hashParams.get('token'));
+ } catch (_) {
+ return '';
+ }
+ }
+
+ async function probeToken(token) {
+ if (!token) return false;
+ try {
+ const res = await fetch('/network', {
+ cache: 'no-store',
+ headers: { 'X-ZT1-Auth': token }
+ });
+ return res.ok;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ async function fetchAuthtoken() {
+ for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
+ try {
+ const res = await fetch(path, { cache: 'no-store' });
+ if (res.ok) {
+ const data = await res.json();
+ const token = trimToken(data && data.token);
+ if (token) return token;
+ }
+ } catch (_) {
+ /* try next */
+ }
+ }
+ return '';
+ }
+
+ async function resolveToken() {
+ const urlToken = tokenFromUrl();
+ if (urlToken) {
+ if (await probeToken(urlToken)) {
+ return urlToken;
+ }
+ }
+
+ const stored = trimToken(localStorage.getItem(LS_KEY));
+ if (stored && await probeToken(stored)) {
+ return stored;
+ }
+
+ const apiToken = await fetchAuthtoken();
+ if (apiToken && await probeToken(apiToken)) {
+ return apiToken;
+ }
+
+ if (stored) return stored;
+ if (apiToken) return apiToken;
+ if (urlToken) return urlToken;
+ return '';
+ }
+
+ async function getToken() {
+ if (cache) return cache;
+ cache = await resolveToken();
+ return cache || '';
+ }
+
+ async function authHeaders(extra) {
+ const token = await getToken();
+ const headers = Object.assign({}, extra || {});
+ if (token) {
+ headers['X-ZT1-Auth'] = token;
+ }
+ return headers;
+ }
+
+ async function fetchWithAuth(path, options) {
+ options = options || {};
+ const headers = await authHeaders(options.headers || {});
+ return fetch(path, Object.assign({}, options, { headers }));
+ }
+
+ async function apiGet(path) {
+ const res = await fetchWithAuth(path, { cache: 'no-store' });
+ if (!res.ok) throw new Error(`${path} returned ${res.status}`);
+ return res.json();
+ }
+
+ async function apiGetFirst(paths) {
+ let lastErr = null;
+ for (const path of paths) {
+ try {
+ return await apiGet(path);
+ } catch (err) {
+ lastErr = err;
+ }
+ }
+ throw lastErr || new Error('API request failed');
+ }
+
+ async function apiSend(path, method, body) {
+ const headers = await authHeaders(
+ body != null ? { 'Content-Type': 'application/json' } : {}
+ );
+ const res = await fetch(path, {
+ method,
+ headers,
+ body: body != null ? JSON.stringify(body) : undefined
+ });
+ if (!res.ok) {
+ const text = await res.text().catch(() => '');
+ throw new Error(text || `${method} ${path} returned ${res.status}`);
+ }
+ if (res.status === 204) {
+ return { result: true };
+ }
+ const text = await res.text();
+ if (!text) {
+ return { result: true };
+ }
+ try {
+ return JSON.parse(text);
+ } catch (_) {
+ return { result: true };
+ }
+ }
+
+ async function apiSendFirst(paths, method, body) {
+ let lastErr = null;
+ for (const path of paths) {
+ try {
+ return await apiSend(path, method, body);
+ } catch (err) {
+ lastErr = err;
+ }
+ }
+ throw lastErr || new Error(`${method} failed`);
+ }
+
+ /** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
+ async function setUserToken(token) {
+ cache = trimToken(token);
+ if (cache) {
+ localStorage.setItem(LS_KEY, cache);
+ } else {
+ localStorage.removeItem(LS_KEY);
+ }
+ return cache;
+ }
+
+ async function validateCurrentToken() {
+ cache = null;
+ const token = await getToken();
+ if (!token) return false;
+ return probeToken(token);
+ }
+
+ function invalidate() {
+ cache = null;
+ }
+
+ async function init() {
+ return getToken();
+ }
+
+ return {
+ getToken,
+ authHeaders,
+ fetchWithAuth,
+ apiGet,
+ apiGetFirst,
+ apiSend,
+ apiSendFirst,
+ setUserToken,
+ validateCurrentToken,
+ invalidate,
+ init
+ };
+})();
diff --git a/Support/BackOne/app/contacts-store.js b/Support/BackOne/app/contacts-store.js
new file mode 100644
index 0000000..01a0cd8
--- /dev/null
+++ b/Support/BackOne/app/contacts-store.js
@@ -0,0 +1,57 @@
+window.BackOneContacts = (function () {
+ let cache = null;
+
+ async function load() {
+ if (cache) return cache;
+ try {
+ const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
+ .catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
+ if (!res.ok) {
+ cache = {};
+ return cache;
+ }
+ cache = await res.json();
+ if (!cache || typeof cache !== 'object') cache = {};
+ } catch (_) {
+ cache = {};
+ }
+ return cache;
+ }
+
+ function getDisplayName(peerId, controllerName) {
+ const c = cache || {};
+ const entry = c[peerId];
+ if (entry && entry.nickname) return entry.nickname;
+ if (controllerName) return controllerName;
+ return peerId;
+ }
+
+ function getNickname(peerId) {
+ const entry = (cache || {})[peerId];
+ return entry && entry.nickname ? entry.nickname : '';
+ }
+
+ async function setNickname(peerId, nickname) {
+ await load();
+ const patch = {};
+ patch[peerId] = { nickname: nickname || '' };
+ const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch)
+ }).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch)
+ }));
+ if (!res.ok) throw new Error('Failed to save nickname');
+ cache = await res.json();
+ return cache;
+ }
+
+ function invalidate() {
+ cache = null;
+ }
+
+ return { load, getDisplayName, getNickname, setNickname, invalidate };
+})();
diff --git a/Support/BackOne/app/index.html b/Support/BackOne/app/index.html
new file mode 100644
index 0000000..e44c47a
--- /dev/null
+++ b/Support/BackOne/app/index.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+ BackOne
+
+
+
+
+
+ Select an addon from the navigation bar.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Support/BackOne/app/main.js b/Support/BackOne/app/main.js
new file mode 100644
index 0000000..a4daa4f
--- /dev/null
+++ b/Support/BackOne/app/main.js
@@ -0,0 +1,228 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+const addonButtons = {};
+let addonModules = {};
+let coreRefreshTimer = null;
+
+function hashAddonId() {
+ const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
+ if (h === 'voice' || h === 'video' || h === 'chat') return h;
+ return '';
+}
+
+function activateAddon(id) {
+ const btn = addonButtons[id];
+ if (!btn) return;
+ btn.click();
+}
+
+function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+}
+
+function networkLabel(n) {
+ if (!n) return '';
+ return n.name ? `${n.name} (${n.id})` : n.id;
+}
+
+function renderCoreStatus(statusEl) {
+ const core = BackOneNetworkSession.getCoreStatus();
+ const net = BackOneNetworkSession.getActiveNetwork();
+ if (!core.online) {
+ statusEl.innerHTML = ' BackOne offline — start backone service';
+ return;
+ }
+ const netPart = net ? networkLabel(net) : 'no network joined';
+ const addr = core.address ? ` · ${escapeHtml(core.address)}` : '';
+ const authNote = core.authenticated
+ ? ' · auth token OK'
+ : ' · paste ~/.backOneOneAuthToken below ';
+ statusEl.innerHTML =
+ ` BackOne online${addr} · ` +
+ `${escapeHtml(netPart)} · ${core.leafPeerCount} peer(s)${authNote}`;
+}
+
+async function initAuthTokenBar(statusEl, onSaved) {
+ const bar = document.getElementById('auth-token-bar');
+ const input = document.getElementById('auth-token-input');
+ const saveBtn = document.getElementById('auth-token-save');
+ if (!bar || !input || !saveBtn) return;
+
+ async function refreshAuthUi() {
+ const ok = await BackOneAuth.validateCurrentToken();
+ bar.classList.toggle('auth-token-hidden', ok);
+ if (!ok && !input.value) {
+ input.placeholder = 'Paste from ~/.backOneOneAuthToken';
+ }
+ return ok;
+ }
+
+ saveBtn.addEventListener('click', async () => {
+ const token = input.value.trim();
+ if (!token) {
+ if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
+ return;
+ }
+ saveBtn.disabled = true;
+ await BackOneAuth.setUserToken(token);
+ BackOneAuth.invalidate();
+ const ok = await BackOneAuth.validateCurrentToken();
+ if (!ok) {
+ if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken matches the running backone service';
+ saveBtn.disabled = false;
+ return;
+ }
+ input.value = '';
+ bar.classList.add('auth-token-hidden');
+ if (typeof onSaved === 'function') await onSaved();
+ saveBtn.disabled = false;
+ });
+
+ await refreshAuthUi();
+ return refreshAuthUi;
+}
+
+async function initNetworkBar() {
+ const bar = document.getElementById('network-bar');
+ if (!bar) return;
+
+ const netSel = bar.querySelector('#global-network');
+ const joinInput = bar.querySelector('#global-join-id');
+ const joinBtn = bar.querySelector('#global-join-btn');
+ const leaveBtn = bar.querySelector('#global-leave-btn');
+ const statusEl = bar.querySelector('#global-network-status');
+
+ async function reloadNetworkList(selectId) {
+ await BackOneNetworkSession.syncFromCore();
+ const networks = await BackOneNetworkSession.loadNetworks();
+ netSel.innerHTML = networks.map(n =>
+ `${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || '')} `
+ ).join('') || '— no networks joined — ';
+
+ const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
+ if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
+ netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
+ await BackOneNetworkSession.selectNetwork(netSel.value);
+ } else if (networks.length === 1) {
+ netSel.value = networks[0].id;
+ await BackOneNetworkSession.selectNetwork(networks[0].id);
+ } else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
+ netSel.value = BackOneNetworkSession.getNetworkId();
+ }
+ renderCoreStatus(statusEl);
+ }
+
+ netSel.addEventListener('change', async () => {
+ if (!netSel.value) return;
+ statusEl.textContent = 'Switching network…';
+ await BackOneNetworkSession.selectNetwork(netSel.value);
+ await BackOneNetworkSession.syncFromCore();
+ renderCoreStatus(statusEl);
+ });
+
+ joinBtn.addEventListener('click', async () => {
+ const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
+ if (!id) {
+ statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
+ return;
+ }
+ joinBtn.disabled = true;
+ leaveBtn.disabled = true;
+ statusEl.textContent = `Joining ${id}…`;
+ try {
+ await BackOneNetworkSession.joinNetwork(id);
+ joinInput.value = '';
+ await reloadNetworkList(id);
+ } catch (err) {
+ statusEl.textContent = err.message || 'Join failed';
+ } finally {
+ joinBtn.disabled = false;
+ leaveBtn.disabled = false;
+ }
+ });
+
+ leaveBtn.addEventListener('click', async () => {
+ const id = netSel.value || BackOneNetworkSession.getNetworkId();
+ if (!id) {
+ statusEl.textContent = 'Select a network to leave';
+ return;
+ }
+ if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
+ joinBtn.disabled = true;
+ leaveBtn.disabled = true;
+ statusEl.textContent = `Leaving ${id}…`;
+ try {
+ await BackOneNetworkSession.leaveNetwork(id);
+ await reloadNetworkList();
+ } catch (err) {
+ statusEl.textContent = err.message || 'Leave failed';
+ } finally {
+ joinBtn.disabled = false;
+ leaveBtn.disabled = false;
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => renderCoreStatus(statusEl));
+
+ const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
+
+ await reloadNetworkList();
+ if (refreshAuthUi) await refreshAuthUi();
+
+ coreRefreshTimer = setInterval(() => {
+ BackOneNetworkSession.syncFromCore().then(() => renderCoreStatus(statusEl));
+ }, 45000);
+}
+
+async function initAppShell() {
+ await BackOneAuth.init();
+ await initNetworkBar();
+
+ const nav = document.getElementById('addon-nav');
+ const panel = document.getElementById('addon-panel');
+ const addons = await BackOneAddonLoader.loadAddonManifest();
+ if (!addons.length) {
+ panel.innerHTML = 'No UI addons found. Install backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: sudo backone-updater rebuild-manifest and restart backone.
';
+ return;
+ }
+
+ for (const entry of addons) {
+ const mod = await BackOneAddonLoader.loadAddonModule(entry);
+ if (mod) addonModules[entry.id] = mod;
+ const btn = document.createElement('button');
+ btn.dataset.addonId = entry.id;
+ btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
+ btn.onclick = () => {
+ nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ panel.innerHTML = '';
+ if (addonModules[entry.id] && addonModules[entry.id].mount) {
+ addonModules[entry.id].mount(panel);
+ } else {
+ panel.innerHTML = `Addon "${entry.id}" loaded.
`;
+ }
+ };
+ addonButtons[entry.id] = btn;
+ nav.appendChild(btn);
+ }
+
+ const fromHash = hashAddonId();
+ if (fromHash && addonButtons[fromHash]) {
+ activateAddon(fromHash);
+ } else {
+ nav.querySelector('button')?.click();
+ }
+}
+
+window.addEventListener('hashchange', () => {
+ const id = hashAddonId();
+ if (id) activateAddon(id);
+});
+
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ BackOneNetworkSession.syncFromCore();
+ }
+});
+
+document.addEventListener('DOMContentLoaded', initAppShell);
diff --git a/Support/BackOne/app/network-session.js b/Support/BackOne/app/network-session.js
new file mode 100644
index 0000000..4a394a6
--- /dev/null
+++ b/Support/BackOne/app/network-session.js
@@ -0,0 +1,178 @@
+/**
+ * Single BackOne core network for all addons (same as backone-cli listnetworks/join).
+ */
+window.BackOneNetworkSession = (function () {
+ const NETWORK_KEY = 'backone.lastNetworkId';
+ const PEER_KEY = 'backone.lastPeerId';
+
+ let activeNetwork = null;
+ let selectedPeer = null;
+ let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
+ const listeners = new Set();
+
+ function emit() {
+ const snap = {
+ network: activeNetwork,
+ networkId: getNetworkId(),
+ peer: selectedPeer,
+ core: { ...coreStatus }
+ };
+ listeners.forEach(fn => {
+ try { fn(snap); } catch (_) { /* ignore */ }
+ });
+ }
+
+ function getNetworkId() {
+ return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
+ }
+
+ function setNetworkId(id) {
+ const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
+ if (nwid) {
+ localStorage.setItem(NETWORK_KEY, nwid);
+ } else {
+ localStorage.removeItem(NETWORK_KEY);
+ }
+ BackOnePeerDirectory.setLastNetworkId(nwid);
+ return nwid;
+ }
+
+ function getSelectedPeerId() {
+ return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
+ }
+
+ function setSelectedPeer(peer) {
+ selectedPeer = peer || null;
+ if (peer && peer.peerId) {
+ localStorage.setItem(PEER_KEY, peer.peerId);
+ } else {
+ localStorage.removeItem(PEER_KEY);
+ }
+ emit();
+ }
+
+ function pickDefaultNetworkId(networks) {
+ if (!networks.length) return '';
+ const stored = getNetworkId();
+ if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
+ return stored;
+ }
+ const preferred = networks.find(n => n.status === 'OK')
+ || networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
+ || networks[0];
+ return preferred ? (preferred.id || '').toLowerCase() : '';
+ }
+
+ /**
+ * On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
+ */
+ async function syncFromCore() {
+ const token = await BackOneAuth.getToken();
+ const [status, networks, cliPeers] = await Promise.all([
+ BackOnePeerDirectory.loadCoreStatus(),
+ BackOnePeerDirectory.loadNetworks(),
+ BackOnePeerDirectory.loadCliPeers()
+ ]);
+
+ coreStatus = {
+ online: !!(status && status.online),
+ address: (status && status.address) || '',
+ version: (status && status.version) || '',
+ networkCount: networks.length,
+ leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
+ authenticated: !!token
+ };
+
+ const pick = pickDefaultNetworkId(networks);
+ if (pick) {
+ setNetworkId(pick);
+ activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
+ || await BackOnePeerDirectory.loadNetwork(pick);
+ } else {
+ setNetworkId('');
+ activeNetwork = null;
+ }
+
+ emit();
+ return { coreStatus, activeNetwork, networks };
+ }
+
+ async function refresh() {
+ const id = getNetworkId();
+ if (!id) {
+ activeNetwork = null;
+ emit();
+ return null;
+ }
+ activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
+ if (!activeNetwork) {
+ activeNetwork = { id, name: id, status: 'UNKNOWN' };
+ }
+ emit();
+ return activeNetwork;
+ }
+
+ async function loadNetworks() {
+ return BackOnePeerDirectory.loadNetworks();
+ }
+
+ async function selectNetwork(nwid) {
+ const id = setNetworkId(nwid);
+ if (!id) {
+ await refresh();
+ return null;
+ }
+ return refresh();
+ }
+
+ async function joinNetwork(nwid) {
+ const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
+ if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
+ await BackOnePeerDirectory.joinNetwork(id);
+ return syncFromCore();
+ }
+
+ async function leaveNetwork(nwid) {
+ const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
+ if (!id) throw new Error('No network selected');
+ await BackOnePeerDirectory.leaveNetwork(id);
+ if (getNetworkId() === id) {
+ setNetworkId('');
+ selectedPeer = null;
+ localStorage.removeItem(PEER_KEY);
+ }
+ return syncFromCore();
+ }
+
+ function subscribe(fn) {
+ listeners.add(fn);
+ return () => listeners.delete(fn);
+ }
+
+ function requireNetworkId() {
+ const id = getNetworkId();
+ if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
+ return id;
+ }
+
+ function getCoreStatus() {
+ return { ...coreStatus };
+ }
+
+ return {
+ getNetworkId,
+ getActiveNetwork: () => activeNetwork,
+ getCoreStatus,
+ getSelectedPeer: () => selectedPeer,
+ getSelectedPeerId,
+ setSelectedPeer,
+ selectNetwork,
+ joinNetwork,
+ leaveNetwork,
+ loadNetworks,
+ syncFromCore,
+ refresh,
+ subscribe,
+ requireNetworkId
+ };
+})();
diff --git a/Support/BackOne/app/peer-directory.js b/Support/BackOne/app/peer-directory.js
new file mode 100644
index 0000000..cff0511
--- /dev/null
+++ b/Support/BackOne/app/peer-directory.js
@@ -0,0 +1,266 @@
+window.BackOnePeerDirectory = (function () {
+ const NETWORK_KEY = 'backone.lastNetworkId';
+ const API = {
+ peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
+ networks: ['/api/backone/networks', '/app/api/networks', '/network'],
+ status: ['/api/backone/status', '/app/api/status', '/status'],
+ network: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ],
+ members: (id) => [
+ `/api/backone/network/${id}/members`,
+ `/app/api/network/${id}/members`,
+ `/unstable/controller/network/${id}/member`
+ ],
+ join: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ],
+ leave: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ]
+ };
+
+ function apiGet(path) {
+ return BackOneAuth.apiGet(path);
+ }
+
+ /** Same data as `backone-cli -j peers` (GET /peer). */
+ async function loadCliPeers() {
+ try {
+ const peers = await BackOneAuth.apiGetFirst(API.peers);
+ return Array.isArray(peers) ? peers : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ /** Same data as `backone-cli -j listnetworks` (GET /network). */
+ async function loadNetworks() {
+ try {
+ const nets = await BackOneAuth.apiGetFirst(API.networks);
+ return Array.isArray(nets) ? nets : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ /** Same data as `backone-cli -j info` (GET /status). */
+ async function loadCoreStatus() {
+ try {
+ return await BackOneAuth.apiGetFirst(API.status);
+ } catch (_) {
+ try {
+ const res = await fetch('/health', { cache: 'no-store' });
+ return { online: res.ok };
+ } catch (_2) {
+ return { online: false };
+ }
+ }
+ }
+
+ function countLeafPeers(cliPeers) {
+ if (!Array.isArray(cliPeers)) return 0;
+ return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
+ }
+
+ async function loadNetwork(nwid) {
+ if (!nwid) return null;
+ const networks = await loadNetworks();
+ const found = networks.find(n => n.id === nwid);
+ if (found) return found;
+ try {
+ return await BackOneAuth.apiGetFirst(API.network(nwid));
+ } catch (_) {
+ return null;
+ }
+ }
+
+ function getLastNetworkId() {
+ return localStorage.getItem(NETWORK_KEY) || '';
+ }
+
+ function setLastNetworkId(nwid) {
+ if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
+ }
+
+ function peerIsOnline(p) {
+ if (!p) return false;
+ const paths = p.paths || [];
+ return paths.some(x => x.active && !x.expired);
+ }
+
+ function peerLinkLabel(p) {
+ if (!p) return '';
+ if (p.tunneled) return 'RELAY';
+ const paths = p.paths || [];
+ for (const path of paths) {
+ if (path.preferred && path.active && !path.expired) {
+ return 'DIRECT';
+ }
+ }
+ for (const path of paths) {
+ if (path.active && !path.expired) {
+ return 'DIRECT';
+ }
+ }
+ return 'RELAY';
+ }
+
+ function peerPreferredPath(p) {
+ if (!p) return '';
+ const paths = p.paths || [];
+ for (const path of paths) {
+ if (path.preferred && path.address) return path.address;
+ }
+ for (const path of paths) {
+ if (path.active && path.address) return path.address;
+ }
+ return '';
+ }
+
+ function cliPeerMap(cliPeers) {
+ const map = {};
+ for (const p of cliPeers) {
+ const id = (p.address || '').toLowerCase();
+ if (id) map[id] = p;
+ }
+ return map;
+ }
+
+ async function loadMemberMap(nwid) {
+ if (!nwid) return {};
+ try {
+ const data = await BackOneAuth.apiGetFirst(API.members(nwid));
+ const map = {};
+ if (data && Array.isArray(data.data)) {
+ for (const m of data.data) {
+ const id = (m.id || '').toLowerCase();
+ if (id) map[id] = m;
+ }
+ }
+ return map;
+ } catch (_) {
+ return {};
+ }
+ }
+
+ function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
+ const controllerName = member ? (member.name || '') : '';
+ const ips = member && member.ipAssignments ? member.ipAssignments : [];
+ return {
+ peerId,
+ networkId: networkId || '',
+ networkName: networkName || '',
+ displayName: BackOneContacts.getDisplayName(peerId, controllerName),
+ controllerName,
+ nickname: BackOneContacts.getNickname(peerId),
+ online: peerIsOnline(cliPeer),
+ latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
+ role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
+ version: (cliPeer && cliPeer.version) || '',
+ link: cliPeer ? peerLinkLabel(cliPeer) : '',
+ path: cliPeer ? peerPreferredPath(cliPeer) : '',
+ ip: ips.length ? ips[0] : '',
+ onNetwork: !!member
+ };
+ }
+
+ async function loadPeersForNetwork(nwid) {
+ if (!nwid) return [];
+
+ const [cliPeers, members, selfStatus, network] = await Promise.all([
+ loadCliPeers(),
+ loadMemberMap(nwid),
+ loadCoreStatus().catch(() => ({})),
+ loadNetwork(nwid)
+ ]);
+ await BackOneContacts.load();
+
+ const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
+ const networkId = nwid;
+ const networkName = network ? (network.name || network.id || nwid) : nwid;
+ const cliById = cliPeerMap(cliPeers);
+ const out = [];
+
+ if (Object.keys(members).length > 0) {
+ for (const peerId of Object.keys(members)) {
+ const member = members[peerId];
+ if (!member || member.authorized === false) continue;
+ if (peerId === selfId) continue;
+ out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
+ }
+ } else {
+ for (const peerId of Object.keys(cliById)) {
+ const p = cliById[peerId];
+ if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
+ if (p.role && p.role !== 'LEAF') continue;
+ out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
+ }
+ }
+
+ out.sort((a, b) => {
+ if (a.online !== b.online) return a.online ? -1 : 1;
+ return a.displayName.localeCompare(b.displayName);
+ });
+ return out;
+ }
+
+ function searchPeers(query, list) {
+ const q = (query || '').trim().toLowerCase();
+ if (!q) return list;
+ return list.filter(p =>
+ p.displayName.toLowerCase().includes(q) ||
+ p.peerId.includes(q) ||
+ (p.networkName || '').toLowerCase().includes(q) ||
+ (p.networkId || '').includes(q) ||
+ (p.controllerName || '').toLowerCase().includes(q) ||
+ (p.role || '').toLowerCase().includes(q) ||
+ (p.path || '').toLowerCase().includes(q) ||
+ (p.ip || '').toLowerCase().includes(q)
+ );
+ }
+
+ function normalizeNetworkId(input) {
+ const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
+ return id.length === 16 ? id : '';
+ }
+
+ async function joinNetwork(nwid) {
+ const id = normalizeNetworkId(nwid);
+ if (!id) {
+ throw new Error('Network ID must be 16 hexadecimal characters');
+ }
+ return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
+ }
+
+ async function leaveNetwork(nwid) {
+ const id = normalizeNetworkId(nwid);
+ if (!id) {
+ throw new Error('Network ID must be 16 hexadecimal characters');
+ }
+ return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
+ }
+
+ return {
+ loadCliPeers,
+ loadNetworks,
+ loadNetwork,
+ loadCoreStatus,
+ countLeafPeers,
+ loadPeersForNetwork,
+ getLastNetworkId,
+ setLastNetworkId,
+ joinNetwork,
+ leaveNetwork,
+ normalizeNetworkId,
+ searchPeers,
+ peerIsOnline,
+ peerLinkLabel
+ };
+})();
diff --git a/Support/BackOne/app/peer-picker.js b/Support/BackOne/app/peer-picker.js
new file mode 100644
index 0000000..9eb830f
--- /dev/null
+++ b/Support/BackOne/app/peer-picker.js
@@ -0,0 +1,151 @@
+window.BackOnePeerPicker = function PeerPicker(container, options) {
+ options = options || {};
+ const onSelect = options.onSelect || (() => {});
+ const onNetworkChange = options.onNetworkChange || (() => {});
+ const onAction = options.onAction || null;
+ const actionLabel = options.actionLabel || 'Select';
+ const showActionButton = options.showActionButton !== false;
+ const useSharedNetwork = options.useSharedNetwork !== false;
+
+ let selectedPeer = null;
+ let peers = [];
+ let currentNetwork = null;
+ let unsub = null;
+
+ container.innerHTML = `
+
+
BackOne core · same network as backone-cli
+
+
Search
+
+
+
+
+ ${showActionButton ? `
${actionLabel} ` : ''}
+
`;
+
+ const netBanner = container.querySelector('#pp-network-banner');
+ const searchInput = container.querySelector('#pp-search');
+ const listEl = container.querySelector('#pp-list');
+ const selectedEl = container.querySelector('#pp-selected');
+ const actionBtn = container.querySelector('#pp-action');
+
+ function networkLabel(n) {
+ if (!n) return '';
+ return n.name ? `${n.name} (${n.id})` : n.id;
+ }
+
+ function renderNetworkBanner() {
+ if (!useSharedNetwork) return;
+ const id = BackOneNetworkSession.getNetworkId();
+ const core = BackOneNetworkSession.getCoreStatus();
+ if (!core.online) {
+ netBanner.innerHTML = 'BackOne offline — peers from backone-cli unavailable ';
+ return;
+ }
+ if (!currentNetwork || !id) {
+ netBanner.innerHTML = 'No network — core reports ' + core.networkCount + ' joined (use header bar or backone-cli listnetworks) ';
+ return;
+ }
+ netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
+ }
+
+ function renderList() {
+ const q = searchInput.value;
+ const filtered = BackOnePeerDirectory.searchPeers(q, peers);
+ const netTag = currentNetwork ? escapeHtml(currentNetwork.name || currentNetwork.id) : '';
+ listEl.innerHTML = filtered.map(p => {
+ const lat = p.latency != null ? `${p.latency}ms` : '—';
+ const meta = [p.role || 'LEAF', lat, p.link || ''].filter(Boolean).join(' · ');
+ const ip = p.ip ? ` · ${escapeHtml(p.ip)}` : '';
+ return `
+
+
+
+ ${escapeHtml(p.displayName)}
+ ${escapeHtml(p.peerId)} · ${escapeHtml(meta)}${netTag ? ' · ' + netTag : ''}${ip}
+
+ ✎
+ `;
+ }).join('') || 'No peers on the active network — check backone-cli peers and controller members ';
+
+ listEl.querySelectorAll('.peer-item').forEach(li => {
+ li.addEventListener('click', (ev) => {
+ if (ev.target.classList.contains('peer-edit')) return;
+ const id = li.dataset.id;
+ selectedPeer = peers.find(x => x.peerId === id) || null;
+ if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
+ renderList();
+ renderSelected();
+ if (selectedPeer) onSelect(selectedPeer, currentNetwork);
+ });
+ const editBtn = li.querySelector('.peer-edit');
+ if (editBtn) {
+ editBtn.addEventListener('click', async (ev) => {
+ ev.stopPropagation();
+ const id = li.dataset.id;
+ const peer = peers.find(x => x.peerId === id);
+ if (!peer) return;
+ const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
+ if (nick === null) return;
+ await BackOneContacts.setNickname(peer.peerId, nick);
+ BackOneContacts.invalidate();
+ await refreshPeers();
+ });
+ }
+ });
+ }
+
+ function renderSelected() {
+ if (!selectedPeer) {
+ selectedEl.textContent = '';
+ if (actionBtn) actionBtn.disabled = true;
+ return;
+ }
+ const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
+ selectedEl.innerHTML = `Selected: ${escapeHtml(selectedPeer.displayName)} (${escapeHtml(selectedPeer.peerId)})${net ? ` on ${escapeHtml(net)} ` : ''}`;
+ if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
+ }
+
+ function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+ }
+
+ async function refreshPeers() {
+ currentNetwork = BackOneNetworkSession.getActiveNetwork();
+ if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
+ currentNetwork = await BackOneNetworkSession.refresh();
+ }
+ renderNetworkBanner();
+ const nwid = BackOneNetworkSession.getNetworkId();
+ await BackOneContacts.load();
+ peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
+ const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
+ if (lastPeerId) {
+ selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
+ }
+ renderList();
+ renderSelected();
+ onNetworkChange(currentNetwork, peers);
+ }
+
+ searchInput.addEventListener('input', renderList);
+ if (actionBtn) {
+ actionBtn.addEventListener('click', () => {
+ if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
+ });
+ }
+
+ if (useSharedNetwork) {
+ unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
+ }
+
+ refreshPeers();
+
+ return {
+ getSelectedPeer: () => selectedPeer,
+ getSelectedNetwork: () => currentNetwork,
+ refresh: refreshPeers,
+ destroy: () => { if (unsub) unsub(); }
+ };
+};
diff --git a/Support/BackOne/app/styles.css b/Support/BackOne/app/styles.css
new file mode 100644
index 0000000..fa92292
--- /dev/null
+++ b/Support/BackOne/app/styles.css
@@ -0,0 +1,53 @@
+body { font-family: system-ui, sans-serif; margin: 0; background: #0f1419; color: #e7ecf1; }
+header { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #1a2332; border-bottom: 1px solid #2d3a4d; flex-wrap: wrap; }
+header h1 { margin: 0; font-size: 1.25rem; }
+.network-bar { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; flex: 1; min-width: 280px; }
+.network-bar-label { font-size: 0.85rem; color: #8b9bb4; }
+.network-bar select, .network-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; }
+.network-bar input { width: 10rem; }
+.network-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
+.network-bar-status { font-size: 0.8rem; color: #6b8cae; flex: 1 1 100%; }
+.auth-token-bar { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 100%; }
+.auth-token-hidden { display: none; }
+.auth-token-label { font-size: 0.85rem; color: #8b9bb4; }
+.auth-token-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; width: 16rem; }
+.auth-token-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
+.core-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.35rem; vertical-align: middle; }
+.core-dot.online { background: #2ecc71; }
+.core-dot.offline { background: #e74c3c; }
+nav { display: flex; gap: 0.5rem; flex-wrap: wrap; }
+nav button { background: #2d5a8a; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; }
+nav button.active { background: #4a90d9; }
+main { padding: 1.5rem; }
+.ringing-hidden { display: none; }
+#ringing-popup { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
+#ringing-popup.ringing-hidden { display: none; }
+.ringing-card { background: #1a2332; padding: 2rem; border-radius: 12px; text-align: center; min-width: 280px; }
+.ringing-card button { margin: 0.5rem; padding: 0.6rem 1.2rem; border: none; border-radius: 6px; cursor: pointer; }
+.ringing-accept { background: #2ecc71; color: #000; }
+.ringing-reject { background: #e74c3c; color: #fff; }
+.peer-picker { max-width: 480px; }
+.peer-picker-core { font-size: 0.75rem; color: #6b8cae; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.04em; }
+.peer-network-banner { font-size: 0.85rem; color: #8b9bb4; margin-bottom: 0.75rem; padding: 0.4rem 0.5rem; background: #1a2332; border-radius: 4px; }
+.peer-network-warn { color: #e6a23c; }
+.peer-picker-row { display: block; margin-bottom: 0.75rem; }
+.peer-picker-row select, .peer-picker-row input { width: 100%; margin-top: 0.25rem; padding: 0.4rem; background: #1a2332; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; }
+.peer-list { list-style: none; padding: 0; margin: 0.5rem 0; max-height: 280px; overflow: auto; border: 1px solid #2d3a4d; border-radius: 6px; }
+.peer-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; cursor: pointer; border-bottom: 1px solid #2d3a4d; }
+.peer-item:last-child { border-bottom: none; }
+.peer-item:hover, .peer-item.active { background: #243044; }
+.peer-dot { width: 8px; height: 8px; border-radius: 50%; background: #666; flex-shrink: 0; }
+.peer-item.online .peer-dot { background: #2ecc71; }
+.peer-main { flex: 1; min-width: 0; }
+.peer-name { display: block; }
+.peer-sub { color: #8b9bb4; font-size: 0.8rem; }
+.peer-edit { background: transparent; border: none; color: #8b9bb4; cursor: pointer; padding: 0.25rem; }
+.peer-empty { padding: 1rem; color: #8b9bb4; }
+.peer-selected { margin: 0.5rem 0; min-height: 1.25rem; }
+.chat-layout { display: flex; gap: 1rem; min-height: 400px; }
+.chat-peers { flex: 0 0 280px; }
+.chat-thread-panel { flex: 1; display: flex; flex-direction: column; }
+.chat-thread-header { padding-bottom: 0.5rem; border-bottom: 1px solid #2d3a4d; margin-bottom: 0.5rem; }
+#chat-thread { flex: 1; overflow: auto; border: 1px solid #2d3a4d; padding: 0.5rem; border-radius: 6px; margin-bottom: 0.5rem; }
+.video-remote-label { position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.6); padding: 2px 6px; font-size: 0.75rem; border-radius: 4px; }
+.video-wrap { position: relative; display: inline-block; }
diff --git a/addons/chat/addon.json b/addons/chat/addon.json
new file mode 100644
index 0000000..6b0ccb6
--- /dev/null
+++ b/addons/chat/addon.json
@@ -0,0 +1,30 @@
+{
+ "id": "chat",
+ "name": "Chat P2P",
+ "version": "1.17.1",
+ "requires": ["comms-core", "webhub"],
+ "backoneCore": {
+ "apis": [
+ "/app/api/peers",
+ "/app/api/networks",
+ "POST /app/api/network/{nwid}",
+ "DELETE /app/api/network/{nwid}",
+ "/app/api/network/{nwid}",
+ "/app/api/network/{nwid}/members",
+ "/app/api/authtoken",
+ "/app/api/contacts"
+ ]
+ },
+ "backend": {
+ "routes": ["/controller/comms/network"],
+ "websocketEvents": ["chat_message", "typing"]
+ },
+ "frontend": {
+ "entry": "/app/addons/chat/index.js",
+ "navLabel": "Chat",
+ "icon": "chat"
+ },
+ "settings": {
+ "attachmentMaxBytes": 10485760
+ }
+}
diff --git a/addons/chat/frontend/index.js b/addons/chat/frontend/index.js
new file mode 100644
index 0000000..9688588
--- /dev/null
+++ b/addons/chat/frontend/index.js
@@ -0,0 +1,197 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+window.BackOneAddons.chat = function chatAddon() {
+ const messagesByPeer = {};
+ let selectedPeer = null;
+ let root = null;
+ let unsubChat = null;
+
+ function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+ }
+
+ function clearSessionMessages() {
+ Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
+ }
+
+ function getThread(peerId) {
+ if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
+ return messagesByPeer[peerId];
+ }
+
+ function formatTime(ts) {
+ if (!ts) return '';
+ const d = new Date(ts);
+ return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
+ }
+
+ function networkLabel() {
+ const n = BackOneNetworkSession.getActiveNetwork();
+ return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
+ }
+
+ function updateConnectionBar() {
+ const bar = root?.querySelector('#chat-conn-bar');
+ if (!bar || !window.BackOneCommLayout) return;
+ bar.innerHTML = BackOneCommLayout.connectionBarHtml(
+ selectedPeer,
+ BackOneNetworkSession.getActiveNetwork()
+ );
+ }
+
+ function renderHeader() {
+ const header = root?.querySelector('#chat-header');
+ if (!header) return;
+ const net = networkLabel();
+ if (!selectedPeer) {
+ header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
+ return;
+ }
+ header.innerHTML = `${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)} ${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''} `;
+ updateConnectionBar();
+ }
+
+ function selfName() {
+ const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
+ return addr ? addr.slice(0, 10) + '…' : 'me';
+ }
+
+ return {
+ mount(container) {
+ root = container;
+ clearSessionMessages();
+ container.innerHTML = `
+
+
`;
+
+ const thread = container.querySelector('#chat-thread');
+ const input = container.querySelector('#chat-input');
+ const sendBtn = container.querySelector('#chat-send');
+
+ const render = () => {
+ if (!selectedPeer) {
+ thread.innerHTML = 'Choose a contact from the list
';
+ return;
+ }
+ const messages = getThread(selectedPeer.peerId);
+ if (!messages.length) {
+ thread.innerHTML = 'No messages yet — start the conversation
';
+ return;
+ }
+ thread.innerHTML = messages.map(m => `
+
+
+ ${escapeHtml(m.senderName || m.sender || 'peer')}
+ ${escapeHtml(formatTime(m.at))}
+
+ ${m.body ? `
${escapeHtml(m.body)}
` : ''}
+
`).join('');
+ thread.scrollTop = thread.scrollHeight;
+ };
+
+ function updateInputs() {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ const ok = !!networkId && !!selectedPeer;
+ input.disabled = !ok;
+ sendBtn.disabled = !ok;
+ renderHeader();
+ }
+
+ if (unsubChat) unsubChat();
+ unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
+ const senderId = (msg.senderId || '').toLowerCase();
+ const peerId = selectedPeer?.peerId?.toLowerCase();
+ if (!peerId || senderId !== peerId) {
+ if (senderId) {
+ getThread(senderId).push({
+ sender: senderId,
+ senderName: msg.senderName || senderId,
+ body: msg.body || '',
+ at: msg.at || Date.now()
+ });
+ }
+ return;
+ }
+ getThread(peerId).push({
+ sender: senderId,
+ senderName: msg.senderName || senderId,
+ body: msg.body || '',
+ at: msg.at || Date.now()
+ });
+ render();
+ });
+
+ BackOnePeerPicker(container.querySelector('#chat-peer-picker'), {
+ layout: 'sidebar',
+ showActionButton: false,
+ onSelect(peer) {
+ selectedPeer = peer;
+ updateInputs();
+ render();
+ },
+ onNetworkChange() {
+ selectedPeer = null;
+ clearSessionMessages();
+ updateInputs();
+ render();
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => {
+ updateInputs();
+ render();
+ });
+
+ sendBtn.onclick = () => {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ if (!selectedPeer || !networkId) return;
+ const body = input.value.trim();
+ if (!body) return;
+ const now = Date.now();
+ const name = selfName();
+ BackOneWebHubClient.send({
+ type: 'chat_message',
+ networkId,
+ targetId: selectedPeer.peerId,
+ senderName: name,
+ body
+ });
+ getThread(selectedPeer.peerId).push({
+ sender: 'me',
+ senderName: name,
+ body,
+ at: now
+ });
+ input.value = '';
+ render();
+ };
+
+ updateInputs();
+ render();
+ }
+ };
+};
diff --git a/addons/comms-core/addon.json b/addons/comms-core/addon.json
new file mode 100644
index 0000000..dae6899
--- /dev/null
+++ b/addons/comms-core/addon.json
@@ -0,0 +1,10 @@
+{
+ "id": "comms-core",
+ "name": "Comms Core",
+ "version": "1.17.1",
+ "requires": [],
+ "backend": {
+ "routes": ["/controller/comms"],
+ "websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
+ }
+}
diff --git a/addons/comms-core/schema.sql b/addons/comms-core/schema.sql
new file mode 100644
index 0000000..a7d5a83
--- /dev/null
+++ b/addons/comms-core/schema.sql
@@ -0,0 +1,32 @@
+-- BackOne comms-core addon schema (PostgreSQL)
+CREATE TABLE IF NOT EXISTS backone_calls (
+ id TEXT PRIMARY KEY,
+ network_id TEXT NOT NULL,
+ caller_id TEXT NOT NULL,
+ callee_id TEXT NOT NULL,
+ call_type TEXT NOT NULL CHECK (call_type IN ('voice', 'video')),
+ state TEXT NOT NULL,
+ sdp_offer TEXT,
+ sdp_answer TEXT,
+ created_at BIGINT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS backone_chat_messages (
+ id TEXT PRIMARY KEY,
+ network_id TEXT NOT NULL,
+ sender_id TEXT NOT NULL,
+ body TEXT,
+ attachment_id TEXT,
+ created_at BIGINT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS backone_attachments (
+ id TEXT PRIMARY KEY,
+ network_id TEXT NOT NULL,
+ sender_id TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ size_bytes BIGINT NOT NULL,
+ storage_path TEXT NOT NULL,
+ created_at BIGINT NOT NULL
+);
diff --git a/addons/secure-chat/addon.json b/addons/secure-chat/addon.json
new file mode 100644
index 0000000..42dba0d
--- /dev/null
+++ b/addons/secure-chat/addon.json
@@ -0,0 +1,27 @@
+{
+ "id": "secure-chat",
+ "name": "Secure Chat",
+ "version": "1.17.1",
+ "requires": ["comms-core", "webhub"],
+ "backoneCore": {
+ "apis": [
+ "/app/api/peers",
+ "/app/api/networks",
+ "POST /app/api/network/{nwid}",
+ "DELETE /app/api/network/{nwid}",
+ "/app/api/network/{nwid}",
+ "/app/api/network/{nwid}/members",
+ "/app/api/authtoken",
+ "/app/api/contacts"
+ ]
+ },
+ "backend": {
+ "routes": ["/controller/comms/network"],
+ "websocketEvents": ["secure_chat_message"]
+ },
+ "frontend": {
+ "entry": "/app/addons/secure-chat/index.js",
+ "navLabel": "Secure Chat",
+ "icon": "chat"
+ }
+}
diff --git a/addons/secure-chat/frontend/index.js b/addons/secure-chat/frontend/index.js
new file mode 100644
index 0000000..c4110f6
--- /dev/null
+++ b/addons/secure-chat/frontend/index.js
@@ -0,0 +1,186 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+window.BackOneAddons['secure-chat'] = function secureChatAddon() {
+ const messagesByPeer = {};
+ let selectedPeer = null;
+ let root = null;
+ let unsubChat = null;
+
+ function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+ }
+
+ function clearSessionMessages() {
+ Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
+ }
+
+ function getThread(peerId) {
+ if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
+ return messagesByPeer[peerId];
+ }
+
+ function formatTime(ts) {
+ if (!ts) return '';
+ const d = new Date(ts);
+ return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
+ }
+
+ function networkLabel() {
+ const n = BackOneNetworkSession.getActiveNetwork();
+ return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
+ }
+
+ function renderHeader() {
+ const header = root?.querySelector('#secure-chat-header');
+ if (!header) return;
+ const net = networkLabel();
+ if (!selectedPeer) {
+ header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
+ return;
+ }
+ header.innerHTML = `${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)} ${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''} `;
+ }
+
+ function selfName() {
+ const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
+ return addr ? addr.slice(0, 10) + '…' : 'me';
+ }
+
+ return {
+ mount(container) {
+ root = container;
+ clearSessionMessages();
+ container.innerHTML = `
+ `;
+
+ const thread = container.querySelector('#secure-chat-thread');
+ const input = container.querySelector('#secure-chat-input');
+ const sendBtn = container.querySelector('#secure-chat-send');
+
+ const render = () => {
+ if (!selectedPeer) {
+ thread.innerHTML = 'Choose a contact from the list
';
+ return;
+ }
+ const messages = getThread(selectedPeer.peerId);
+ if (!messages.length) {
+ thread.innerHTML = 'No messages yet — start the conversation
';
+ return;
+ }
+ thread.innerHTML = messages.map(m => `
+
+
+ ${escapeHtml(m.senderName || m.sender || 'me')}
+ ${escapeHtml(formatTime(m.at))}
+
+ ${m.body ? `
${escapeHtml(m.body)}
` : ''}
+
`).join('');
+ thread.scrollTop = thread.scrollHeight;
+ };
+
+ function updateInputs() {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ const ok = !!networkId && !!selectedPeer;
+ input.disabled = !ok;
+ sendBtn.disabled = !ok;
+ renderHeader();
+ }
+
+ if (unsubChat) unsubChat();
+ unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
+ const senderId = (msg.senderId || '').toLowerCase();
+ const peerId = selectedPeer?.peerId?.toLowerCase();
+ if (!peerId || senderId !== peerId) {
+ if (senderId) {
+ getThread(senderId).push({
+ sender: senderId,
+ senderName: msg.senderName || senderId,
+ body: msg.body || '',
+ at: msg.at || Date.now()
+ });
+ }
+ return;
+ }
+ getThread(peerId).push({
+ sender: senderId,
+ senderName: msg.senderName || senderId,
+ body: msg.body || '',
+ at: msg.at || Date.now()
+ });
+ render();
+ });
+
+ BackOnePeerPicker(container.querySelector('#secure-chat-peer-picker'), {
+ layout: 'sidebar',
+ showActionButton: false,
+ onSelect(peer) {
+ selectedPeer = peer;
+ updateInputs();
+ render();
+ },
+ onNetworkChange() {
+ selectedPeer = null;
+ clearSessionMessages();
+ updateInputs();
+ render();
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => {
+ updateInputs();
+ render();
+ });
+
+ sendBtn.onclick = () => {
+ const networkId = BackOneNetworkSession.getNetworkId();
+ if (!selectedPeer || !networkId) return;
+ const body = input.value.trim();
+ if (!body) return;
+ const now = Date.now();
+ const name = selfName();
+ BackOneWebHubClient.send({
+ type: 'chat_message',
+ networkId,
+ targetId: selectedPeer.peerId,
+ senderName: name,
+ body
+ });
+ getThread(selectedPeer.peerId).push({
+ sender: 'me',
+ senderName: name,
+ body,
+ at: now
+ });
+ input.value = '';
+ render();
+ };
+
+ updateInputs();
+ render();
+ }
+ };
+};
diff --git a/addons/updater/addon.json b/addons/updater/addon.json
new file mode 100644
index 0000000..e68031d
--- /dev/null
+++ b/addons/updater/addon.json
@@ -0,0 +1,10 @@
+{
+ "id": "updater",
+ "name": "BackOne Updater",
+ "version": "1.17.1",
+ "requires": [],
+ "backend": {
+ "routes": [],
+ "websocketEvents": []
+ }
+}
diff --git a/addons/updater/backone-updater b/addons/updater/backone-updater
new file mode 100755
index 0000000..2b17b41
--- /dev/null
+++ b/addons/updater/backone-updater
@@ -0,0 +1,132 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
+ # shellcheck source=platform-paths.sh
+ . "${SCRIPT_DIR}/platform-paths.sh"
+else
+ # shellcheck source=../../pkg/common/platform-paths.sh
+ . "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
+fi
+backone_resolve_paths
+
+HOME_DIR="${BACKONE_HOME}"
+STAMP="${HOME_DIR}/.installed-version"
+LOG_DIR="${BACKONE_LOG_DIR:-/var/log/backone}"
+LOG_FILE="${LOG_DIR}/updater.log"
+MIGRATIONS="${BACKONE_ADDONS_DIR}/updater/migrations"
+LEGACY_HOME="/var/lib/zerotier-one"
+
+log() {
+ mkdir -p "$LOG_DIR"
+ echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
+}
+
+read_version() {
+ if [ -f "$STAMP" ]; then
+ cat "$STAMP"
+ else
+ echo '{"package":"backone","version":"0.0.0","schemaVersion":0}'
+ fi
+}
+
+write_version() {
+ local version="${1:-$(cat debian/version 2>/dev/null || echo 1.15.0)}"
+ mkdir -p "$HOME_DIR"
+ cat > "$STAMP" </dev/null || true
+}
+
+run_migrations() {
+ local scope="${1:-all}"
+ [ -d "$MIGRATIONS" ] || return 0
+ for script in "$MIGRATIONS"/*; do
+ [ -f "$script" ] || continue
+ case "$(basename "$script")" in
+ 001_*)
+ [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
+ ;;
+ 002_*)
+ [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
+ ;;
+ 003_*)
+ [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
+ ;;
+ 004_*|005_*)
+ [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
+ ;;
+ *)
+ if [ "$scope" != "all" ] && [ "$scope" != "$ADDON_SCOPE" ]; then
+ continue
+ fi
+ ;;
+ esac
+ log "Running migration: $(basename "$script")"
+ BACKONE_HOME="$HOME_DIR" bash "$script" || log "Migration warning: $(basename "$script")"
+ done
+}
+
+migrate_zerotier() {
+ if [ -d "$LEGACY_HOME" ] && [ ! -f "$HOME_DIR/identity.public" ]; then
+ log "Migrating $LEGACY_HOME -> $HOME_DIR"
+ mkdir -p "$HOME_DIR"
+ cp -a "$LEGACY_HOME/." "$HOME_DIR/" 2>/dev/null || true
+ ln -sfn "$HOME_DIR" "$LEGACY_HOME" 2>/dev/null || true
+ fi
+}
+
+rebuild_manifest() {
+ local registry="${HOME_DIR}/addons.d/enabled.json"
+ mkdir -p "${HOME_DIR}/addons.d"
+ if [ ! -f "$registry" ]; then
+ echo '[]' > "$registry"
+ fi
+ local script_dir manifest_script
+ script_dir="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
+ manifest_script="${script_dir}/rebuild-addon-manifest.sh"
+ if [ ! -x "$manifest_script" ]; then
+ echo "backone-updater: ${manifest_script} not found" >&2
+ exit 1
+ fi
+ local count
+ count="$(BACKONE_HOME="$HOME_DIR" BACKONE_ADDONS_DIR="${BACKONE_ADDONS_DIR}" bash "$manifest_script")"
+ log "Wrote ${HOME_DIR}/app/addons/manifest.json (${count} UI addons)"
+}
+
+cmd_migrate() {
+ local scope="all"
+ ADDON_SCOPE=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --core) scope="core"; shift ;;
+ --addon) ADDON_SCOPE="$2"; scope="addon"; shift 2 ;;
+ --dry-run)
+ log "Dry run: would migrate scope=$scope addon=$ADDON_SCOPE"
+ ls -1 "$MIGRATIONS" 2>/dev/null || true
+ exit 0
+ ;;
+ *) shift ;;
+ esac
+ done
+ migrate_zerotier
+ run_migrations "$scope"
+ write_version
+ rebuild_manifest
+ log "Migration complete"
+}
+
+cmd_status() {
+ echo "Installed: $(read_version)"
+ echo "Package version: $(cat "${BACKONE_ADDONS_DIR}/comms-core/addon.json" 2>/dev/null | grep '"version"' | head -1 || cat debian/version 2>/dev/null || echo unknown)"
+ echo "Addons registry: ${HOME_DIR}/addons.d/enabled.json"
+}
+
+case "${1:-}" in
+ migrate) shift; cmd_migrate "$@" ;;
+ status) cmd_status ;;
+ rebuild-manifest) rebuild_manifest ;;
+ *) echo "Usage: backone-updater migrate [--core|--addon ID] [--dry-run] | status | rebuild-manifest" >&2; exit 1 ;;
+esac
diff --git a/addons/updater/merge-local-conf.sh b/addons/updater/merge-local-conf.sh
new file mode 100644
index 0000000..5fd7fee
--- /dev/null
+++ b/addons/updater/merge-local-conf.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Deep-merge JSON into /var/lib/backone/local.conf (idempotent).
+set -euo pipefail
+
+HOME_DIR="${BACKONE_HOME:-/var/lib/backone}"
+CONF="${HOME_DIR}/local.conf"
+PATCH="${1:-}"
+
+if [ -z "$PATCH" ] && [ ! -t 0 ]; then
+ PATCH="$(cat)"
+fi
+
+if [ -z "$PATCH" ]; then
+ echo "Usage: merge-local-conf.sh '' or pipe JSON on stdin" >&2
+ exit 1
+fi
+
+python3 - "$CONF" "$PATCH" <<'PY'
+import json
+import os
+import sys
+
+conf_path, patch_raw = sys.argv[1], sys.argv[2]
+
+def deep_merge(base, patch):
+ if not isinstance(base, dict) or not isinstance(patch, dict):
+ return patch
+ out = dict(base)
+ for k, v in patch.items():
+ if k in out and isinstance(out[k], dict) and isinstance(v, dict):
+ out[k] = deep_merge(out[k], v)
+ else:
+ out[k] = v
+ return out
+
+patch = json.loads(patch_raw)
+current = {}
+if os.path.isfile(conf_path):
+ with open(conf_path, encoding="utf-8") as f:
+ try:
+ current = json.load(f)
+ except json.JSONDecodeError:
+ current = {}
+if not isinstance(current, dict):
+ current = {}
+
+merged = deep_merge(current, patch)
+os.makedirs(os.path.dirname(conf_path), exist_ok=True)
+with open(conf_path, "w", encoding="utf-8") as f:
+ json.dump(merged, f, indent=2)
+ f.write("\n")
+PY
+
+chown backone:backone "$CONF" 2>/dev/null || true
+chmod 644 "$CONF" 2>/dev/null || true
diff --git a/addons/updater/migrations/001_zerotier_to_backone.sh b/addons/updater/migrations/001_zerotier_to_backone.sh
new file mode 100755
index 0000000..5010d84
--- /dev/null
+++ b/addons/updater/migrations/001_zerotier_to_backone.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -euo pipefail
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+LEGACY="/var/lib/zerotier-one"
+if [ -d "$LEGACY" ] && [ ! -f "$BACKONE_HOME/identity.public" ]; then
+ mkdir -p "$BACKONE_HOME"
+ cp -an "$LEGACY/." "$BACKONE_HOME/" 2>/dev/null || cp -a "$LEGACY/." "$BACKONE_HOME/"
+fi
+if ! getent passwd backone >/dev/null 2>&1 && getent passwd zerotier-one >/dev/null 2>&1; then
+ usermod -l backone -d "$BACKONE_HOME" zerotier-one 2>/dev/null || true
+fi
+chown -R backone:backone "$BACKONE_HOME" 2>/dev/null || true
diff --git a/addons/updater/migrations/002_register_addon_host.sh b/addons/updater/migrations/002_register_addon_host.sh
new file mode 100755
index 0000000..469fb7f
--- /dev/null
+++ b/addons/updater/migrations/002_register_addon_host.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -euo pipefail
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+mkdir -p "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app/addons"
+if [ ! -f "$BACKONE_HOME/addons.d/enabled.json" ]; then
+ echo '[]' > "$BACKONE_HOME/addons.d/enabled.json"
+fi
+chown -R backone:backone "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app" 2>/dev/null || true
diff --git a/addons/updater/migrations/003_enable_webserver_defaults.sh b/addons/updater/migrations/003_enable_webserver_defaults.sh
new file mode 100644
index 0000000..ad94e07
--- /dev/null
+++ b/addons/updater/migrations/003_enable_webserver_defaults.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -euo pipefail
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
+ BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{"settings":{"enableWebServer":true}}'
+fi
diff --git a/addons/updater/migrations/004_webrtc_ice_defaults.sh b/addons/updater/migrations/004_webrtc_ice_defaults.sh
new file mode 100755
index 0000000..e1694a5
--- /dev/null
+++ b/addons/updater/migrations/004_webrtc_ice_defaults.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -euo pipefail
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
+ BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{
+ "settings": {
+ "icePreferOverlay": true,
+ "iceServers": [{"urls": "stun:stun.l.google.com:19302"}],
+ "iceFallbackTimeoutMs": 8000,
+ "webhubPort": 9994
+ }
+ }'
+fi
diff --git a/addons/updater/migrations/005_enable_webhub_defaults.sh b/addons/updater/migrations/005_enable_webhub_defaults.sh
new file mode 100755
index 0000000..3053353
--- /dev/null
+++ b/addons/updater/migrations/005_enable_webhub_defaults.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -euo pipefail
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+CONF="${BACKONE_HOME}/local.conf"
+
+python3 - "$CONF" <<'PY'
+import json
+import os
+import sys
+
+path = sys.argv[1]
+defaults = ["comms-core", "webhub", "voice", "video", "chat"]
+current = {}
+if os.path.isfile(path):
+ with open(path, encoding="utf-8") as f:
+ try:
+ current = json.load(f)
+ except json.JSONDecodeError:
+ current = {}
+if not isinstance(current, dict):
+ current = {}
+settings = current.setdefault("settings", {})
+if not isinstance(settings, dict):
+ settings = {}
+ current["settings"] = settings
+enabled = settings.get("enabledAddons")
+if enabled is None:
+ # No explicit list — all installed addons stay enabled.
+ sys.exit(0)
+if not isinstance(enabled, list):
+ enabled = []
+merged = list(enabled)
+for addon in defaults:
+ if addon not in merged:
+ merged.append(addon)
+settings["enabledAddons"] = merged
+os.makedirs(os.path.dirname(path), exist_ok=True)
+with open(path, "w", encoding="utf-8") as f:
+ json.dump(current, f, indent=2)
+ f.write("\n")
+PY
+
+chown backone:backone "$CONF" 2>/dev/null || true
+chmod 644 "$CONF" 2>/dev/null || true
diff --git a/addons/updater/rebuild-addon-manifest.sh b/addons/updater/rebuild-addon-manifest.sh
new file mode 100755
index 0000000..21b5418
--- /dev/null
+++ b/addons/updater/rebuild-addon-manifest.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+# Writes $BACKONE_HOME/app/addons/manifest.json from installed addon.json files.
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
+ # shellcheck source=platform-paths.sh
+ . "${SCRIPT_DIR}/platform-paths.sh"
+else
+ # shellcheck source=../../pkg/common/platform-paths.sh
+ . "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
+fi
+backone_resolve_paths
+
+HOME_DIR="${BACKONE_HOME}"
+ADDONS_DIR="${BACKONE_ADDONS_DIR}"
+MANIFEST="${HOME_DIR}/app/addons/manifest.json"
+
+mkdir -p "${HOME_DIR}/app/addons" "${HOME_DIR}/addons.d"
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "rebuild-addon-manifest: python3 required" >&2
+ exit 1
+fi
+
+python3 - "$ADDONS_DIR" "$MANIFEST" <<'PY'
+import json
+import os
+import sys
+
+addons_dir, manifest_path = sys.argv[1], sys.argv[2]
+out = []
+if os.path.isdir(addons_dir):
+ for name in sorted(os.listdir(addons_dir)):
+ path = os.path.join(addons_dir, name, "addon.json")
+ if not os.path.isfile(path):
+ continue
+ with open(path, encoding="utf-8") as f:
+ m = json.load(f)
+ if "frontend" not in m:
+ continue
+ item = {
+ "id": m["id"],
+ "name": m.get("name", m["id"]),
+ "version": m.get("version", "0.0.0"),
+ "frontend": m["frontend"],
+ }
+ if "requires" in m:
+ item["requires"] = m["requires"]
+ out.append(item)
+
+os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
+with open(manifest_path, "w", encoding="utf-8") as f:
+ json.dump(out, f, indent=2)
+ f.write("\n")
+print(len(out))
+PY
+
+chown backone:backone "$MANIFEST" 2>/dev/null || true
+chmod 644 "$MANIFEST" 2>/dev/null || true
diff --git a/addons/updater/sync-authtoken.sh b/addons/updater/sync-authtoken.sh
new file mode 100755
index 0000000..13232e9
--- /dev/null
+++ b/addons/updater/sync-authtoken.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
+set -e
+
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+SECRET="${BACKONE_HOME}/authtoken.secret"
+
+trim_token() {
+ tr -d '\n\r \t' < "$1"
+}
+
+sync_for_user() {
+ user="$1"
+ home="$2"
+ [ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
+ user_token="${home}/.backOneOneAuthToken"
+
+ if [ -f "$user_token" ] && [ -s "$user_token" ]; then
+ ut="$(trim_token "$user_token")"
+ [ -n "$ut" ] || return 0
+ st=""
+ if [ -f "$SECRET" ]; then
+ st="$(trim_token "$SECRET" 2>/dev/null || true)"
+ fi
+ if [ "$ut" != "$st" ]; then
+ printf '%s' "$ut" > "$SECRET"
+ chown backone:backone "$SECRET" 2>/dev/null || true
+ chmod 0600 "$SECRET" 2>/dev/null || true
+ echo "Synced ${SECRET} from ${user_token}"
+ fi
+ return 0
+ fi
+
+ if [ -f "$SECRET" ]; then
+ install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
+ || cp "$SECRET" "$user_token"
+ chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
+ chmod 0600 "$user_token" 2>/dev/null || true
+ echo "Installed ${user_token} from ${SECRET}"
+ fi
+}
+
+if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
+ home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
+ sync_for_user "$SUDO_USER" "$home"
+fi
+
+if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
+ home="$(getent passwd "$USER" | cut -d: -f6)"
+ sync_for_user "$USER" "$home"
+fi
+
+exit 0
diff --git a/addons/video/addon.json b/addons/video/addon.json
new file mode 100644
index 0000000..18b8d9e
--- /dev/null
+++ b/addons/video/addon.json
@@ -0,0 +1,30 @@
+{
+ "id": "video",
+ "name": "Video Call P2P",
+ "version": "1.17.1",
+ "requires": ["comms-core", "webhub", "voice"],
+ "backoneCore": {
+ "apis": [
+ "/app/api/peers",
+ "/app/api/networks",
+ "POST /app/api/network/{nwid}",
+ "DELETE /app/api/network/{nwid}",
+ "/app/api/network/{nwid}",
+ "/app/api/network/{nwid}/members",
+ "/app/api/authtoken",
+ "/app/api/contacts"
+ ]
+ },
+ "backend": {
+ "routes": ["/controller/comms/call"],
+ "websocketEvents": ["incoming_call", "call_state"]
+ },
+ "frontend": {
+ "entry": "/app/addons/video/index.js",
+ "navLabel": "Video",
+ "icon": "video"
+ },
+ "settings": {
+ "enableVideoCall": true
+ }
+}
diff --git a/addons/video/frontend/index.js b/addons/video/frontend/index.js
new file mode 100644
index 0000000..8e7b7dc
--- /dev/null
+++ b/addons/video/frontend/index.js
@@ -0,0 +1,226 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+window.BackOneAddons.video = function videoAddon() {
+ let picker = null;
+ let selectedPeer = null;
+ let inCall = false;
+ let micEnabled = true;
+ let camEnabled = true;
+ let root = null;
+
+ function updateConnectionBar() {
+ const bar = root?.querySelector('#video-conn-bar');
+ if (!bar || !window.BackOneCommLayout) return;
+ bar.innerHTML = BackOneCommLayout.connectionBarHtml(
+ selectedPeer,
+ BackOneNetworkSession.getActiveNetwork()
+ );
+ }
+
+ function updateHeadStatus() {
+ const statusEl = root?.querySelector('#video-head-status');
+ const breadcrumb = root?.querySelector('#video-breadcrumb-peer');
+ if (!statusEl) return;
+ if (inCall && selectedPeer) {
+ statusEl.innerHTML = `Connected · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
+ if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
+ } else if (selectedPeer) {
+ statusEl.textContent = 'Ready';
+ if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
+ } else {
+ statusEl.textContent = '';
+ if (breadcrumb) breadcrumb.textContent = '—';
+ }
+ }
+
+ function updateCallButtons() {
+ const callBtn = root?.querySelector('#video-call');
+ if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
+ const selectedLabel = root?.querySelector('#video-selected-label');
+ if (selectedLabel) {
+ selectedLabel.innerHTML = selectedPeer
+ ? `Selected: ${selectedPeer.peerId.slice(0, 10)}… `
+ : 'Selected: — ';
+ }
+ updateHeadStatus();
+ updateConnectionBar();
+ }
+
+ function updateCallLayout() {
+ const layout = root?.querySelector('.comm-layout');
+ if (layout) layout.classList.toggle('video-in-call', inCall);
+ }
+
+ function showPlaceholder(show) {
+ const stage = root?.querySelector('.video-stage');
+ const remote = root?.querySelector('#video-remote');
+ const phLabel = root?.querySelector('#video-ph-label');
+ if (stage) stage.classList.toggle('has-remote', !show);
+ if (remote) remote.classList.toggle('active', !show);
+ if (phLabel && show) {
+ phLabel.textContent = inCall
+ ? 'Waiting for remote peer…'
+ : (selectedPeer ? 'Ready to call' : 'Select a peer to start a video call');
+ }
+ updateCallLayout();
+ }
+
+ function bindStreams() {
+ const local = root?.querySelector('#video-local');
+ const remote = root?.querySelector('#video-remote');
+ const localStream = BackOneWebRtcSession.getLocalStream();
+ const remoteStream = BackOneWebRtcSession.getRemoteStream();
+ if (local && localStream) local.srcObject = localStream;
+ if (remote && remoteStream && remoteStream.getTracks().length) {
+ remote.srcObject = remoteStream;
+ showPlaceholder(false);
+ root?.querySelector('#video-breadcrumb-state')?.textContent = 'Connected to';
+ }
+ }
+
+ async function hangUp() {
+ await BackOneWebRtcSession.hangUp();
+ const local = root?.querySelector('#video-local');
+ const remote = root?.querySelector('#video-remote');
+ if (local) local.srcObject = null;
+ if (remote) remote.srcObject = null;
+ inCall = false;
+ micEnabled = true;
+ camEnabled = true;
+ const breadcrumbState = root?.querySelector('#video-breadcrumb-state');
+ if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
+ showPlaceholder(true);
+ updateCallButtons();
+ window.BackOneRingingPopup?.dismiss();
+ }
+
+ return {
+ mount(container) {
+ root = container;
+ container.innerHTML = `
+
+
+
+
+
Video Call P2P > Select peer —
+
+
+
+
+
+
👤
+
—
+
Select a peer to start a video call
+
+
+
+
+ You
+
+
+
Remote
+
+ Mic
+ Camera
+ Fullscreen
+ Hang Up
+
+
+
+
+
`;
+
+ const callBtn = container.querySelector('#video-call');
+
+ picker = BackOnePeerPicker(container.querySelector('#video-picker'), {
+ layout: 'sidebar',
+ showActionButton: false,
+ onSelect(peer) {
+ selectedPeer = peer;
+ const phPeer = root.querySelector('#video-ph-peer');
+ if (phPeer) phPeer.textContent = peer.peerId;
+ if (!inCall) showPlaceholder(true);
+ updateCallButtons();
+ },
+ onNetworkChange() {
+ if (!inCall) selectedPeer = null;
+ updateCallButtons();
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => updateCallButtons());
+ BackOneWebRtcSession.subscribe((event) => {
+ if (event === 'call-started') {
+ inCall = true;
+ updateCallButtons();
+ bindStreams();
+ }
+ if (event === 'remote-track' || event === 'call-accepted') bindStreams();
+ if (event === 'call-ended') {
+ inCall = false;
+ showPlaceholder(true);
+ updateCallButtons();
+ }
+ });
+
+ callBtn.onclick = async () => {
+ const peer = selectedPeer || picker.getSelectedPeer();
+ if (!peer || !BackOneNetworkSession.getNetworkId()) return;
+ try {
+ inCall = true;
+ root.querySelector('#video-breadcrumb-state').textContent = 'Connecting to';
+ updateCallLayout();
+ updateCallButtons();
+ await BackOneWebRtcSession.startOutgoingCall(peer, 'video');
+ bindStreams();
+ } catch (err) {
+ await hangUp();
+ alert(err.message || 'Call failed');
+ }
+ };
+
+ container.querySelector('#video-hangup').onclick = () => hangUp();
+ container.querySelector('#video-hangup-stage').onclick = () => hangUp();
+
+ container.querySelector('#video-mic').onclick = () => {
+ const localStream = BackOneWebRtcSession.getLocalStream();
+ if (!localStream) return;
+ micEnabled = !micEnabled;
+ localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
+ container.querySelector('#video-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
+ };
+
+ container.querySelector('#video-cam').onclick = () => {
+ const localStream = BackOneWebRtcSession.getLocalStream();
+ if (!localStream) return;
+ camEnabled = !camEnabled;
+ localStream.getVideoTracks().forEach(t => { t.enabled = camEnabled; });
+ container.querySelector('#video-cam').textContent = camEnabled ? 'Camera' : 'Camera off';
+ };
+
+ container.querySelector('#video-fs').onclick = () => {
+ const stage = container.querySelector('.comm-stage');
+ if (stage.requestFullscreen) stage.requestFullscreen();
+ };
+
+ const active = BackOneWebRtcSession.getActiveCall();
+ inCall = !!active && active.callType === 'video';
+ showPlaceholder(true);
+ if (inCall) bindStreams();
+ updateCallButtons();
+ }
+ };
+};
diff --git a/addons/voice/addon.json b/addons/voice/addon.json
new file mode 100644
index 0000000..77a0927
--- /dev/null
+++ b/addons/voice/addon.json
@@ -0,0 +1,31 @@
+{
+ "id": "voice",
+ "name": "Voice Call P2P",
+ "version": "1.17.1",
+ "requires": ["comms-core", "webhub"],
+ "backoneCore": {
+ "apis": [
+ "/app/api/peers",
+ "/app/api/networks",
+ "POST /app/api/network/{nwid}",
+ "DELETE /app/api/network/{nwid}",
+ "/app/api/network/{nwid}",
+ "/app/api/network/{nwid}/members",
+ "/app/api/authtoken",
+ "/app/api/contacts"
+ ]
+ },
+ "backend": {
+ "routes": ["/controller/comms/call"],
+ "websocketEvents": ["incoming_call", "call_state"]
+ },
+ "frontend": {
+ "entry": "/app/addons/voice/index.js",
+ "navLabel": "Voice",
+ "icon": "phone"
+ },
+ "settings": {
+ "enableVoiceCall": true,
+ "ringTimeoutSec": 30
+ }
+}
diff --git a/addons/voice/frontend/index.js b/addons/voice/frontend/index.js
new file mode 100644
index 0000000..6e23e4c
--- /dev/null
+++ b/addons/voice/frontend/index.js
@@ -0,0 +1,181 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+window.BackOneAddons.voice = function voiceAddon() {
+ let picker = null;
+ let selectedPeer = null;
+ let inCall = false;
+ let micEnabled = true;
+ let root = null;
+ let remoteAudio = null;
+
+ function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+ }
+
+ function updateConnectionBar() {
+ const bar = root?.querySelector('#voice-conn-bar');
+ if (!bar || !window.BackOneCommLayout) return;
+ bar.innerHTML = BackOneCommLayout.connectionBarHtml(
+ selectedPeer,
+ BackOneNetworkSession.getActiveNetwork()
+ );
+ }
+
+ function updateHeadStatus() {
+ const statusEl = root?.querySelector('#voice-head-status');
+ const breadcrumb = root?.querySelector('#voice-breadcrumb-peer');
+ if (!statusEl) return;
+ if (inCall && selectedPeer) {
+ statusEl.innerHTML = `Connected · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
+ if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
+ } else if (selectedPeer) {
+ statusEl.textContent = 'Ready';
+ if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
+ } else {
+ statusEl.textContent = '';
+ if (breadcrumb) breadcrumb.textContent = '—';
+ }
+ }
+
+ function updateActiveBanner() {
+ const banner = root?.querySelector('#voice-active-banner');
+ const label = root?.querySelector('#voice-active-label');
+ if (!banner) return;
+ banner.style.display = inCall && selectedPeer ? 'flex' : 'none';
+ if (label && selectedPeer) {
+ label.textContent = `In call with ${selectedPeer.displayName || selectedPeer.peerId}`;
+ }
+ }
+
+ function updateCallButtons() {
+ const callBtn = root?.querySelector('#voice-call');
+ if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
+ const selectedLabel = root?.querySelector('#voice-selected-label');
+ if (selectedLabel) {
+ selectedLabel.innerHTML = selectedPeer
+ ? `Selected: ${selectedPeer.peerId.slice(0, 10)}… `
+ : 'Selected: — ';
+ }
+ updateHeadStatus();
+ updateConnectionBar();
+ updateActiveBanner();
+ }
+
+ function syncInCallState() {
+ const active = BackOneWebRtcSession.getActiveCall();
+ inCall = !!active && (active.callType === 'voice' || !active.callType);
+ if (inCall && active.peer) {
+ selectedPeer = active.peer.peerId ? active.peer : selectedPeer;
+ root?.querySelector('#voice-breadcrumb-state')?.textContent = 'Connected to';
+ }
+ updateCallButtons();
+ }
+
+ async function hangUp() {
+ await BackOneWebRtcSession.hangUp();
+ inCall = false;
+ micEnabled = true;
+ if (remoteAudio) remoteAudio.srcObject = null;
+ const breadcrumbState = root?.querySelector('#voice-breadcrumb-state');
+ if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
+ updateCallButtons();
+ window.BackOneRingingPopup?.dismiss();
+ }
+
+ return {
+ mount(container) {
+ root = container;
+ container.innerHTML = `
+
+
+
+
+
Voice Call P2P > Select peer —
+
+
+
+
+
+
+ In call
+
+
+ Mic
+ Hang Up
+
+
+
Live calls only — no call history is stored.
+
+
+
+
+
`;
+
+ remoteAudio = container.querySelector('#voice-remote-audio');
+ const callBtn = container.querySelector('#voice-call');
+
+ picker = BackOnePeerPicker(container.querySelector('#voice-picker'), {
+ layout: 'sidebar',
+ showActionButton: false,
+ onSelect(peer) {
+ selectedPeer = peer;
+ updateCallButtons();
+ },
+ onNetworkChange() {
+ if (!inCall) selectedPeer = null;
+ updateCallButtons();
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => updateCallButtons());
+ BackOneWebRtcSession.subscribe((event) => {
+ if (event === 'call-started' || event === 'call-ended' || event === 'remote-track') {
+ syncInCallState();
+ const stream = BackOneWebRtcSession.getRemoteStream();
+ if (remoteAudio && stream) remoteAudio.srcObject = stream;
+ }
+ });
+
+ callBtn.onclick = async () => {
+ const peer = selectedPeer || picker.getSelectedPeer();
+ if (!peer || !BackOneNetworkSession.getNetworkId()) return;
+ try {
+ await BackOneWebRtcSession.startOutgoingCall(peer, 'voice');
+ inCall = true;
+ root.querySelector('#voice-breadcrumb-state').textContent = 'Connecting to';
+ updateCallButtons();
+ } catch (err) {
+ await hangUp();
+ alert(err.message || 'Call failed');
+ }
+ };
+
+ container.querySelector('#voice-hangup').onclick = () => hangUp();
+ container.querySelector('#voice-hangup-stage').onclick = () => hangUp();
+
+ container.querySelector('#voice-mic').onclick = () => {
+ const localStream = BackOneWebRtcSession.getLocalStream();
+ if (!localStream) return;
+ micEnabled = !micEnabled;
+ localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
+ container.querySelector('#voice-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
+ };
+
+ syncInCallState();
+ updateCallButtons();
+ }
+ };
+};
diff --git a/addons/webhub/addon.json b/addons/webhub/addon.json
new file mode 100644
index 0000000..4b20f76
--- /dev/null
+++ b/addons/webhub/addon.json
@@ -0,0 +1,15 @@
+{
+ "id": "webhub",
+ "name": "WebSocket Signaling Hub",
+ "version": "1.17.1",
+ "requires": ["comms-core"],
+ "backend": {
+ "routes": [],
+ "websocketEvents": ["presence", "incoming_call", "call_state", "ice_candidate", "chat_message", "typing"],
+ "sidecar": {
+ "command": "/usr/lib/backone/addons/webhub/backone-webhub",
+ "port": 9994,
+ "bind": "127.0.0.1"
+ }
+ }
+}
diff --git a/addons/webhub/backone-webhub b/addons/webhub/backone-webhub
new file mode 100755
index 0000000..fb02dd3
--- /dev/null
+++ b/addons/webhub/backone-webhub
@@ -0,0 +1,343 @@
+#!/usr/bin/env python3
+"""BackOne WebHub — ephemeral in-memory WebSocket signaling (no DB, no history)."""
+import asyncio
+import json
+import os
+import secrets
+import time
+import urllib.parse
+
+try:
+ import websockets
+ from websockets.server import serve
+except ImportError:
+ raise SystemExit("python3-websockets is required")
+
+BACKONE_HOME = os.environ.get("BACKONE_HOME", "/var/lib/backone")
+AUTH_PATH = os.path.join(BACKONE_HOME, "authtoken.secret")
+CONF_PATH = os.path.join(BACKONE_HOME, "local.conf")
+DEFAULT_PORT = 9994
+RING_TIMEOUT_SEC = 30
+
+peers = {} # peer_id -> {ws, network_id, registered_at}
+calls = {} # call_id -> call dict
+ice_queues = {} # call_id -> list of {from, candidate, ...}
+peer_to_calls = {} # peer_id -> set(call_id)
+
+
+def load_auth_token():
+ try:
+ with open(AUTH_PATH, encoding="utf-8") as f:
+ t = f.read().strip()
+ return t if t else None
+ except OSError:
+ return None
+
+
+def load_port():
+ try:
+ with open(CONF_PATH, encoding="utf-8") as f:
+ cfg = json.load(f)
+ settings = cfg.get("settings") or {}
+ return int(settings.get("webhubPort", DEFAULT_PORT))
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
+ return DEFAULT_PORT
+
+
+def valid_token(token):
+ expected = load_auth_token()
+ if not expected or not token:
+ return False
+ return secrets.compare_digest(token.strip(), expected.strip())
+
+
+def new_call_id():
+ return f"call_{int(time.time() * 1000)}_{secrets.token_hex(4)}"
+
+
+def peer_key(peer_id):
+ return (peer_id or "").lower()
+
+
+def send_json(ws, payload):
+ return ws.send(json.dumps(payload))
+
+
+async def push_to_peer(target_id, payload):
+ key = peer_key(target_id)
+ entry = peers.get(key)
+ if not entry:
+ return False
+ try:
+ await send_json(entry["ws"], payload)
+ return True
+ except Exception:
+ return False
+
+
+def track_call_for_peer(pid, call_id):
+ key = peer_key(pid)
+ if key not in peer_to_calls:
+ peer_to_calls[key] = set()
+ peer_to_calls[key].add(call_id)
+
+
+def untrack_call(call_id, call):
+ for pid in (call.get("callerId"), call.get("calleeId")):
+ key = peer_key(pid)
+ if key in peer_to_calls:
+ peer_to_calls[key].discard(call_id)
+ if not peer_to_calls[key]:
+ del peer_to_calls[key]
+
+
+def purge_call(call_id):
+ call = calls.pop(call_id, None)
+ ice_queues.pop(call_id, None)
+ if call:
+ untrack_call(call_id, call)
+
+
+def end_calls_for_peer(peer_id):
+ key = peer_key(peer_id)
+ for call_id in list(peer_to_calls.get(key, set())):
+ call = calls.get(call_id)
+ if not call:
+ continue
+ other = call["calleeId"] if peer_key(call["callerId"]) == key else call["callerId"]
+ asyncio.create_task(push_to_peer(other, {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "ended",
+ "reason": "peer_disconnect"
+ }))
+ purge_call(call_id)
+
+
+async def ring_timeout_watch():
+ while True:
+ await asyncio.sleep(5)
+ now = time.time()
+ for call_id, call in list(calls.items()):
+ if call.get("state") != "ringing":
+ continue
+ if now - call.get("createdAt", now) > RING_TIMEOUT_SEC:
+ await push_to_peer(call["callerId"], {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "rejected",
+ "reason": "timeout"
+ })
+ purge_call(call_id)
+
+
+async def handle_message(ws, state, msg):
+ mtype = msg.get("type")
+ if mtype == "auth":
+ token = msg.get("token", "")
+ if not valid_token(token):
+ await send_json(ws, {"type": "error", "error": "unauthorized"})
+ await ws.close()
+ return False
+ state["authed"] = True
+ await send_json(ws, {"type": "auth_ok"})
+ return True
+
+ if not state.get("authed"):
+ await send_json(ws, {"type": "error", "error": "not authenticated"})
+ return True
+
+ if mtype == "register":
+ peer_id = peer_key(msg.get("peerId", ""))
+ network_id = (msg.get("networkId") or "").lower()
+ if not peer_id:
+ await send_json(ws, {"type": "error", "error": "peerId required"})
+ return True
+ peers[peer_id] = {"ws": ws, "networkId": network_id, "registeredAt": time.time()}
+ state["peerId"] = peer_id
+ await send_json(ws, {"type": "registered", "peerId": peer_id})
+ return True
+
+ peer_id = state.get("peerId")
+ if not peer_id:
+ await send_json(ws, {"type": "error", "error": "register first"})
+ return True
+
+ if mtype == "call_offer":
+ target_id = peer_key(msg.get("targetId", ""))
+ call_type = msg.get("callType") or "voice"
+ sdp = msg.get("sdp", "")
+ network_id = (msg.get("networkId") or peers[peer_id].get("networkId") or "").lower()
+ if not target_id or not sdp:
+ await send_json(ws, {"type": "error", "error": "targetId and sdp required"})
+ return True
+ call_id = new_call_id()
+ call = {
+ "callId": call_id,
+ "callType": call_type,
+ "networkId": network_id,
+ "callerId": peer_id,
+ "calleeId": target_id,
+ "state": "ringing",
+ "offerSdp": sdp,
+ "answerSdp": "",
+ "createdAt": time.time()
+ }
+ calls[call_id] = call
+ ice_queues[call_id] = []
+ track_call_for_peer(peer_id, call_id)
+ track_call_for_peer(target_id, call_id)
+ delivered = await push_to_peer(target_id, {
+ "type": "incoming_call",
+ "callId": call_id,
+ "callType": call_type,
+ "networkId": network_id,
+ "callerId": peer_id,
+ "callerName": msg.get("callerName", peer_id),
+ "sdp": sdp,
+ "overlayIps": msg.get("overlayIps", [])
+ })
+ await send_json(ws, {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "ringing",
+ "delivered": delivered
+ })
+ return True
+
+ if mtype == "call_answer":
+ call_id = msg.get("callId", "")
+ sdp = msg.get("sdp", "")
+ call = calls.get(call_id)
+ if not call or peer_key(call["calleeId"]) != peer_id:
+ await send_json(ws, {"type": "error", "error": "call not found"})
+ return True
+ call["answerSdp"] = sdp
+ call["state"] = "accepted"
+ await push_to_peer(call["callerId"], {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "accepted",
+ "sdp": sdp
+ })
+ await send_json(ws, {"type": "call_state", "callId": call_id, "state": "accepted"})
+ return True
+
+ if mtype == "call_reject":
+ call_id = msg.get("callId", "")
+ call = calls.get(call_id)
+ if call and peer_key(call["calleeId"]) == peer_id:
+ await push_to_peer(call["callerId"], {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "rejected"
+ })
+ purge_call(call_id)
+ return True
+
+ if mtype == "call_end":
+ call_id = msg.get("callId", "")
+ call = calls.get(call_id)
+ if call and peer_id in (peer_key(call["callerId"]), peer_key(call["calleeId"])):
+ other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
+ await push_to_peer(other, {
+ "type": "call_state",
+ "callId": call_id,
+ "state": "ended"
+ })
+ purge_call(call_id)
+ return True
+
+ if mtype == "ice_candidate":
+ call_id = msg.get("callId", "")
+ call = calls.get(call_id)
+ if not call:
+ return True
+ entry = {
+ "from": peer_id,
+ "candidate": msg.get("candidate"),
+ "sdpMid": msg.get("sdpMid"),
+ "sdpMLineIndex": msg.get("sdpMLineIndex"),
+ "at": time.time()
+ }
+ ice_queues.setdefault(call_id, []).append(entry)
+ other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
+ await push_to_peer(other, {
+ "type": "ice_candidate",
+ "callId": call_id,
+ **{k: entry[k] for k in ("candidate", "sdpMid", "sdpMLineIndex")}
+ })
+ return True
+
+ if mtype == "ice_fetch":
+ call_id = msg.get("callId", "")
+ since = float(msg.get("since", 0))
+ queued = [e for e in ice_queues.get(call_id, []) if e.get("at", 0) > since and e.get("from") != peer_id]
+ await send_json(ws, {"type": "ice_candidates", "callId": call_id, "candidates": queued})
+ return True
+
+ if mtype == "chat_message":
+ target_id = peer_key(msg.get("targetId", ""))
+ if target_id:
+ await push_to_peer(target_id, {
+ "type": "chat_message",
+ "networkId": msg.get("networkId", ""),
+ "senderId": peer_id,
+ "senderName": msg.get("senderName", peer_id),
+ "body": msg.get("body", ""),
+ "at": int(time.time() * 1000)
+ })
+ return True
+
+ if mtype == "typing":
+ target_id = peer_key(msg.get("targetId", ""))
+ if target_id:
+ await push_to_peer(target_id, {
+ "type": "typing",
+ "networkId": msg.get("networkId", ""),
+ "senderId": peer_id,
+ "typing": bool(msg.get("typing", True))
+ })
+ return True
+
+ await send_json(ws, {"type": "error", "error": f"unknown type: {mtype}"})
+ return True
+
+
+async def connection_handler(ws, path):
+ state = {"authed": False, "peerId": None}
+ query = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)
+ if "auth" in query and valid_token(query["auth"][0]):
+ state["authed"] = True
+ await send_json(ws, {"type": "auth_ok"})
+
+ try:
+ async for raw in ws:
+ try:
+ msg = json.loads(raw)
+ except json.JSONDecodeError:
+ await send_json(ws, {"type": "error", "error": "invalid json"})
+ continue
+ if not await handle_message(ws, state, msg):
+ break
+ finally:
+ pid = state.get("peerId")
+ if pid and peers.get(pid, {}).get("ws") is ws:
+ del peers[pid]
+ end_calls_for_peer(pid)
+
+
+async def main():
+ port = load_port()
+ host = "127.0.0.1"
+ asyncio.create_task(ring_timeout_watch())
+ async with serve(connection_handler, host, port, ping_interval=20, ping_timeout=20):
+ print(f"BackOne WebHub listening on ws://{host}:{port}", flush=True)
+ await asyncio.Future()
+
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ pass
diff --git a/addons/webhub/backone-webhub.service b/addons/webhub/backone-webhub.service
new file mode 100644
index 0000000..1739f6f
--- /dev/null
+++ b/addons/webhub/backone-webhub.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=BackOne WebSocket signaling hub (webhub)
+After=network.target backone.service
+Wants=backone.service
+
+[Service]
+Type=simple
+ExecStart=/usr/lib/backone/addons/webhub/backone-webhub
+Restart=always
+RestartSec=3
+User=backone
+Group=backone
+
+[Install]
+WantedBy=multi-user.target
diff --git a/app-shell/dist/addon-loader.js b/app-shell/dist/addon-loader.js
new file mode 100644
index 0000000..5fffc36
--- /dev/null
+++ b/app-shell/dist/addon-loader.js
@@ -0,0 +1,57 @@
+window.BackOneRingingPopup = {
+ _audio: null,
+ showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
+ const el = document.getElementById('ringing-popup');
+ if (!el) return;
+ el.classList.remove('ringing-hidden');
+ el.innerHTML = `
+
+
Incoming ${callType} call
+
${callerName || 'Unknown peer'}
+
Accept
+
Reject
+
`;
+ el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
+ el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
+ this.playRingtone();
+ if (Notification.permission === 'default') Notification.requestPermission();
+ if (Notification.permission === 'granted') {
+ new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
+ }
+ },
+ dismiss() {
+ const el = document.getElementById('ringing-popup');
+ if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
+ this.stopRingtone();
+ },
+ playRingtone() {
+ this.stopRingtone();
+ try {
+ this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
+ this._audio.loop = true;
+ this._audio.play().catch(() => {});
+ } catch (_) {}
+ },
+ stopRingtone() {
+ if (this._audio) { this._audio.pause(); this._audio = null; }
+ }
+};
+
+async function loadAddonManifest() {
+ const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
+ if (!res.ok) {
+ console.warn('manifest.json fetch failed:', res.status);
+ return [];
+ }
+ const data = await res.json();
+ return Array.isArray(data) ? data : [];
+}
+
+async function loadAddonModule(entry) {
+ const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
+ await import(entryPath);
+ const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
+ return factory ? factory() : null;
+}
+
+window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
diff --git a/app-shell/dist/auth-store.js b/app-shell/dist/auth-store.js
new file mode 100644
index 0000000..533f850
--- /dev/null
+++ b/app-shell/dist/auth-store.js
@@ -0,0 +1,195 @@
+/**
+ * BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
+ */
+window.BackOneAuth = (function () {
+ const LS_KEY = 'backone.authToken';
+ let cache = null;
+
+ function trimToken(s) {
+ return String(s || '').replace(/\s+/g, '');
+ }
+
+ function tokenFromUrl() {
+ try {
+ const params = new URLSearchParams(location.search);
+ const fromQuery = trimToken(params.get('auth') || params.get('token'));
+ if (fromQuery) return fromQuery;
+ const hash = (location.hash || '').replace(/^#\/?/, '');
+ const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
+ return trimToken(hashParams.get('auth') || hashParams.get('token'));
+ } catch (_) {
+ return '';
+ }
+ }
+
+ async function probeToken(token) {
+ if (!token) return false;
+ try {
+ const res = await fetch('/network', {
+ cache: 'no-store',
+ headers: { 'X-ZT1-Auth': token }
+ });
+ return res.ok;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ async function fetchAuthtoken() {
+ for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
+ try {
+ const res = await fetch(path, { cache: 'no-store' });
+ if (res.ok) {
+ const data = await res.json();
+ const token = trimToken(data && data.token);
+ if (token) return token;
+ }
+ } catch (_) {
+ /* try next */
+ }
+ }
+ return '';
+ }
+
+ async function resolveToken() {
+ const urlToken = tokenFromUrl();
+ if (urlToken) {
+ if (await probeToken(urlToken)) {
+ return urlToken;
+ }
+ }
+
+ const stored = trimToken(localStorage.getItem(LS_KEY));
+ if (stored && await probeToken(stored)) {
+ return stored;
+ }
+
+ const apiToken = await fetchAuthtoken();
+ if (apiToken && await probeToken(apiToken)) {
+ return apiToken;
+ }
+
+ if (stored) return stored;
+ if (apiToken) return apiToken;
+ if (urlToken) return urlToken;
+ return '';
+ }
+
+ async function getToken() {
+ if (cache) return cache;
+ cache = await resolveToken();
+ return cache || '';
+ }
+
+ async function authHeaders(extra) {
+ const token = await getToken();
+ const headers = Object.assign({}, extra || {});
+ if (token) {
+ headers['X-ZT1-Auth'] = token;
+ }
+ return headers;
+ }
+
+ async function fetchWithAuth(path, options) {
+ options = options || {};
+ const headers = await authHeaders(options.headers || {});
+ return fetch(path, Object.assign({}, options, { headers }));
+ }
+
+ async function apiGet(path) {
+ const res = await fetchWithAuth(path, { cache: 'no-store' });
+ if (!res.ok) throw new Error(`${path} returned ${res.status}`);
+ return res.json();
+ }
+
+ async function apiGetFirst(paths) {
+ let lastErr = null;
+ for (const path of paths) {
+ try {
+ return await apiGet(path);
+ } catch (err) {
+ lastErr = err;
+ }
+ }
+ throw lastErr || new Error('API request failed');
+ }
+
+ async function apiSend(path, method, body) {
+ const headers = await authHeaders(
+ body != null ? { 'Content-Type': 'application/json' } : {}
+ );
+ const res = await fetch(path, {
+ method,
+ headers,
+ body: body != null ? JSON.stringify(body) : undefined
+ });
+ if (!res.ok) {
+ const text = await res.text().catch(() => '');
+ throw new Error(text || `${method} ${path} returned ${res.status}`);
+ }
+ if (res.status === 204) {
+ return { result: true };
+ }
+ const text = await res.text();
+ if (!text) {
+ return { result: true };
+ }
+ try {
+ return JSON.parse(text);
+ } catch (_) {
+ return { result: true };
+ }
+ }
+
+ async function apiSendFirst(paths, method, body) {
+ let lastErr = null;
+ for (const path of paths) {
+ try {
+ return await apiSend(path, method, body);
+ } catch (err) {
+ lastErr = err;
+ }
+ }
+ throw lastErr || new Error(`${method} failed`);
+ }
+
+ /** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
+ async function setUserToken(token) {
+ cache = trimToken(token);
+ if (cache) {
+ localStorage.setItem(LS_KEY, cache);
+ } else {
+ localStorage.removeItem(LS_KEY);
+ }
+ return cache;
+ }
+
+ async function validateCurrentToken() {
+ cache = null;
+ const token = await getToken();
+ if (!token) return false;
+ return probeToken(token);
+ }
+
+ function invalidate() {
+ cache = null;
+ }
+
+ async function init() {
+ return getToken();
+ }
+
+ return {
+ getToken,
+ authHeaders,
+ fetchWithAuth,
+ apiGet,
+ apiGetFirst,
+ apiSend,
+ apiSendFirst,
+ setUserToken,
+ validateCurrentToken,
+ invalidate,
+ init
+ };
+})();
diff --git a/app-shell/dist/contacts-store.js b/app-shell/dist/contacts-store.js
new file mode 100644
index 0000000..01a0cd8
--- /dev/null
+++ b/app-shell/dist/contacts-store.js
@@ -0,0 +1,57 @@
+window.BackOneContacts = (function () {
+ let cache = null;
+
+ async function load() {
+ if (cache) return cache;
+ try {
+ const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
+ .catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
+ if (!res.ok) {
+ cache = {};
+ return cache;
+ }
+ cache = await res.json();
+ if (!cache || typeof cache !== 'object') cache = {};
+ } catch (_) {
+ cache = {};
+ }
+ return cache;
+ }
+
+ function getDisplayName(peerId, controllerName) {
+ const c = cache || {};
+ const entry = c[peerId];
+ if (entry && entry.nickname) return entry.nickname;
+ if (controllerName) return controllerName;
+ return peerId;
+ }
+
+ function getNickname(peerId) {
+ const entry = (cache || {})[peerId];
+ return entry && entry.nickname ? entry.nickname : '';
+ }
+
+ async function setNickname(peerId, nickname) {
+ await load();
+ const patch = {};
+ patch[peerId] = { nickname: nickname || '' };
+ const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch)
+ }).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch)
+ }));
+ if (!res.ok) throw new Error('Failed to save nickname');
+ cache = await res.json();
+ return cache;
+ }
+
+ function invalidate() {
+ cache = null;
+ }
+
+ return { load, getDisplayName, getNickname, setNickname, invalidate };
+})();
diff --git a/app-shell/dist/index.html b/app-shell/dist/index.html
new file mode 100644
index 0000000..dfc64f5
--- /dev/null
+++ b/app-shell/dist/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+ BackOne
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-shell/dist/main.js b/app-shell/dist/main.js
new file mode 100644
index 0000000..8b33f32
--- /dev/null
+++ b/app-shell/dist/main.js
@@ -0,0 +1,265 @@
+window.BackOneAddons = window.BackOneAddons || {};
+
+const addonButtons = {};
+let addonModules = {};
+let coreRefreshTimer = null;
+
+function hashAddonId() {
+ const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
+ if (h === 'voice' || h === 'video' || h === 'chat') return h;
+ return '';
+}
+
+function activateAddon(id) {
+ const btn = addonButtons[id];
+ if (!btn) return;
+ btn.click();
+}
+
+function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+}
+
+function networkLabel(n) {
+ if (!n) return '';
+ return n.name ? `${n.name} (${n.id})` : n.id;
+}
+
+function updateCoreBadge(statusEl) {
+ const badge = document.getElementById('core-status-badge');
+ const core = BackOneNetworkSession.getCoreStatus();
+ if (badge) {
+ badge.textContent = core.online ? 'Online' : 'Offline';
+ badge.classList.toggle('online', core.online);
+ badge.classList.toggle('offline', !core.online);
+ }
+ if (!statusEl) return;
+ if (!core.online) {
+ statusEl.textContent = 'Start the backone service to use networks and peers';
+ return;
+ }
+ const net = BackOneNetworkSession.getActiveNetwork();
+ const netPart = net ? networkLabel(net) : 'no network joined';
+ statusEl.textContent = `${netPart} · ${core.leafPeerCount} peer(s)`;
+}
+
+async function initAuthTokenBar(statusEl, onSaved) {
+ const bar = document.getElementById('auth-token-bar');
+ const input = document.getElementById('auth-token-input');
+ const saveBtn = document.getElementById('auth-token-save');
+ if (!bar || !input || !saveBtn) return;
+
+ async function refreshAuthUi() {
+ const ok = await BackOneAuth.validateCurrentToken();
+ bar.classList.toggle('auth-token-hidden', ok);
+ if (!ok && !input.value) {
+ input.placeholder = 'Paste from ~/.backOneOneAuthToken';
+ }
+ return ok;
+ }
+
+ saveBtn.addEventListener('click', async () => {
+ const token = input.value.trim();
+ if (!token) {
+ if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
+ return;
+ }
+ saveBtn.disabled = true;
+ await BackOneAuth.setUserToken(token);
+ BackOneAuth.invalidate();
+ const ok = await BackOneAuth.validateCurrentToken();
+ if (!ok) {
+ if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken';
+ saveBtn.disabled = false;
+ return;
+ }
+ input.value = '';
+ bar.classList.add('auth-token-hidden');
+ if (typeof onSaved === 'function') await onSaved();
+ saveBtn.disabled = false;
+ });
+
+ await refreshAuthUi();
+ return refreshAuthUi;
+}
+
+async function initNetworkBar() {
+ const bar = document.getElementById('network-bar');
+ if (!bar) return;
+
+ const netSel = bar.querySelector('#global-network');
+ const joinInput = bar.querySelector('#global-join-id');
+ const joinBtn = bar.querySelector('#global-join-btn');
+ const leaveBtn = bar.querySelector('#global-leave-btn');
+ const statusEl = bar.querySelector('#global-network-status');
+
+ async function reloadNetworkList(selectId) {
+ await BackOneNetworkSession.syncFromCore();
+ const networks = await BackOneNetworkSession.loadNetworks();
+ netSel.innerHTML = networks.map(n =>
+ `${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || 'OK')} `
+ ).join('') || '— no networks — ';
+
+ const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
+ if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
+ netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
+ await BackOneNetworkSession.selectNetwork(netSel.value);
+ } else if (networks.length === 1) {
+ netSel.value = networks[0].id;
+ await BackOneNetworkSession.selectNetwork(networks[0].id);
+ } else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
+ netSel.value = BackOneNetworkSession.getNetworkId();
+ }
+ updateCoreBadge(statusEl);
+ }
+
+ netSel.addEventListener('change', async () => {
+ if (!netSel.value) return;
+ statusEl.textContent = 'Switching network…';
+ await BackOneNetworkSession.selectNetwork(netSel.value);
+ await BackOneNetworkSession.syncFromCore();
+ updateCoreBadge(statusEl);
+ });
+
+ joinBtn.addEventListener('click', async () => {
+ const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
+ if (!id) {
+ statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
+ return;
+ }
+ joinBtn.disabled = true;
+ leaveBtn.disabled = true;
+ statusEl.textContent = `Joining ${id}…`;
+ try {
+ await BackOneNetworkSession.joinNetwork(id);
+ joinInput.value = '';
+ await reloadNetworkList(id);
+ } catch (err) {
+ statusEl.textContent = err.message || 'Join failed';
+ } finally {
+ joinBtn.disabled = false;
+ leaveBtn.disabled = false;
+ }
+ });
+
+ leaveBtn.addEventListener('click', async () => {
+ const id = netSel.value || BackOneNetworkSession.getNetworkId();
+ if (!id) {
+ statusEl.textContent = 'Select a network to leave';
+ return;
+ }
+ if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
+ joinBtn.disabled = true;
+ leaveBtn.disabled = true;
+ statusEl.textContent = `Leaving ${id}…`;
+ try {
+ await BackOneNetworkSession.leaveNetwork(id);
+ await reloadNetworkList();
+ } catch (err) {
+ statusEl.textContent = err.message || 'Leave failed';
+ } finally {
+ joinBtn.disabled = false;
+ leaveBtn.disabled = false;
+ }
+ });
+
+ BackOneNetworkSession.subscribe(() => updateCoreBadge(statusEl));
+
+ const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
+
+ await reloadNetworkList();
+ if (refreshAuthUi) await refreshAuthUi();
+
+ coreRefreshTimer = setInterval(() => {
+ BackOneNetworkSession.syncFromCore().then(() => updateCoreBadge(statusEl));
+ }, 45000);
+}
+
+async function initCommsStack() {
+ await BackOneWebHubClient.connect();
+ await BackOneWebRtcSession.init();
+
+ BackOneWebHubClient.subscribe('incoming_call', (msg) => {
+ if (BackOneWebRtcSession.isInCall()) return;
+ BackOneRingingPopup.showIncomingCall({
+ callerName: msg.callerName || msg.callerId,
+ callType: msg.callType || 'voice',
+ callId: msg.callId,
+ onAccept: async () => {
+ const type = msg.callType || 'voice';
+ const addonId = type === 'video' ? 'video' : 'voice';
+ if (addonButtons[addonId]) activateAddon(addonId);
+ try {
+ await BackOneWebRtcSession.acceptIncomingCall(msg);
+ } catch (err) {
+ alert(err.message || 'Could not accept call');
+ }
+ },
+ onReject: (callId) => {
+ BackOneWebRtcSession.rejectIncomingCall(callId || msg.callId);
+ }
+ });
+ });
+}
+
+async function initAppShell() {
+ await BackOneAuth.init();
+ await initNetworkBar();
+ await initCommsStack();
+
+ const nav = document.getElementById('addon-nav');
+ const panel = document.getElementById('addon-panel');
+ const addons = await BackOneAddonLoader.loadAddonManifest();
+ if (!addons.length) {
+ panel.innerHTML = 'No UI addons found. Install backone-addon-secure-chat, backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: sudo backone-updater rebuild-manifest and restart backone.
';
+ return;
+ }
+
+ const navOrder = ['secure-chat', 'chat', 'video', 'voice'];
+ const sorted = [...addons].sort((a, b) => {
+ const ia = navOrder.indexOf(a.id);
+ const ib = navOrder.indexOf(b.id);
+ return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
+ });
+
+ for (const entry of sorted) {
+ const mod = await BackOneAddonLoader.loadAddonModule(entry);
+ if (mod) addonModules[entry.id] = mod;
+ const btn = document.createElement('button');
+ btn.dataset.addonId = entry.id;
+ btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
+ btn.onclick = () => {
+ nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ panel.innerHTML = '';
+ location.hash = entry.id;
+ if (addonModules[entry.id] && addonModules[entry.id].mount) {
+ addonModules[entry.id].mount(panel);
+ } else {
+ panel.innerHTML = `Addon "${entry.id}" loaded.
`;
+ }
+ };
+ addonButtons[entry.id] = btn;
+ nav.appendChild(btn);
+ }
+
+ const fromHash = hashAddonId();
+ if (fromHash && addonButtons[fromHash]) {
+ activateAddon(fromHash);
+ } else {
+ nav.querySelector('button')?.click();
+ }
+}
+
+window.addEventListener('hashchange', () => {
+ const id = hashAddonId();
+ if (id) activateAddon(id);
+});
+
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ BackOneNetworkSession.syncFromCore();
+ }
+});
+
+document.addEventListener('DOMContentLoaded', initAppShell);
diff --git a/app-shell/dist/network-session.js b/app-shell/dist/network-session.js
new file mode 100644
index 0000000..4a394a6
--- /dev/null
+++ b/app-shell/dist/network-session.js
@@ -0,0 +1,178 @@
+/**
+ * Single BackOne core network for all addons (same as backone-cli listnetworks/join).
+ */
+window.BackOneNetworkSession = (function () {
+ const NETWORK_KEY = 'backone.lastNetworkId';
+ const PEER_KEY = 'backone.lastPeerId';
+
+ let activeNetwork = null;
+ let selectedPeer = null;
+ let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
+ const listeners = new Set();
+
+ function emit() {
+ const snap = {
+ network: activeNetwork,
+ networkId: getNetworkId(),
+ peer: selectedPeer,
+ core: { ...coreStatus }
+ };
+ listeners.forEach(fn => {
+ try { fn(snap); } catch (_) { /* ignore */ }
+ });
+ }
+
+ function getNetworkId() {
+ return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
+ }
+
+ function setNetworkId(id) {
+ const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
+ if (nwid) {
+ localStorage.setItem(NETWORK_KEY, nwid);
+ } else {
+ localStorage.removeItem(NETWORK_KEY);
+ }
+ BackOnePeerDirectory.setLastNetworkId(nwid);
+ return nwid;
+ }
+
+ function getSelectedPeerId() {
+ return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
+ }
+
+ function setSelectedPeer(peer) {
+ selectedPeer = peer || null;
+ if (peer && peer.peerId) {
+ localStorage.setItem(PEER_KEY, peer.peerId);
+ } else {
+ localStorage.removeItem(PEER_KEY);
+ }
+ emit();
+ }
+
+ function pickDefaultNetworkId(networks) {
+ if (!networks.length) return '';
+ const stored = getNetworkId();
+ if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
+ return stored;
+ }
+ const preferred = networks.find(n => n.status === 'OK')
+ || networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
+ || networks[0];
+ return preferred ? (preferred.id || '').toLowerCase() : '';
+ }
+
+ /**
+ * On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
+ */
+ async function syncFromCore() {
+ const token = await BackOneAuth.getToken();
+ const [status, networks, cliPeers] = await Promise.all([
+ BackOnePeerDirectory.loadCoreStatus(),
+ BackOnePeerDirectory.loadNetworks(),
+ BackOnePeerDirectory.loadCliPeers()
+ ]);
+
+ coreStatus = {
+ online: !!(status && status.online),
+ address: (status && status.address) || '',
+ version: (status && status.version) || '',
+ networkCount: networks.length,
+ leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
+ authenticated: !!token
+ };
+
+ const pick = pickDefaultNetworkId(networks);
+ if (pick) {
+ setNetworkId(pick);
+ activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
+ || await BackOnePeerDirectory.loadNetwork(pick);
+ } else {
+ setNetworkId('');
+ activeNetwork = null;
+ }
+
+ emit();
+ return { coreStatus, activeNetwork, networks };
+ }
+
+ async function refresh() {
+ const id = getNetworkId();
+ if (!id) {
+ activeNetwork = null;
+ emit();
+ return null;
+ }
+ activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
+ if (!activeNetwork) {
+ activeNetwork = { id, name: id, status: 'UNKNOWN' };
+ }
+ emit();
+ return activeNetwork;
+ }
+
+ async function loadNetworks() {
+ return BackOnePeerDirectory.loadNetworks();
+ }
+
+ async function selectNetwork(nwid) {
+ const id = setNetworkId(nwid);
+ if (!id) {
+ await refresh();
+ return null;
+ }
+ return refresh();
+ }
+
+ async function joinNetwork(nwid) {
+ const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
+ if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
+ await BackOnePeerDirectory.joinNetwork(id);
+ return syncFromCore();
+ }
+
+ async function leaveNetwork(nwid) {
+ const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
+ if (!id) throw new Error('No network selected');
+ await BackOnePeerDirectory.leaveNetwork(id);
+ if (getNetworkId() === id) {
+ setNetworkId('');
+ selectedPeer = null;
+ localStorage.removeItem(PEER_KEY);
+ }
+ return syncFromCore();
+ }
+
+ function subscribe(fn) {
+ listeners.add(fn);
+ return () => listeners.delete(fn);
+ }
+
+ function requireNetworkId() {
+ const id = getNetworkId();
+ if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
+ return id;
+ }
+
+ function getCoreStatus() {
+ return { ...coreStatus };
+ }
+
+ return {
+ getNetworkId,
+ getActiveNetwork: () => activeNetwork,
+ getCoreStatus,
+ getSelectedPeer: () => selectedPeer,
+ getSelectedPeerId,
+ setSelectedPeer,
+ selectNetwork,
+ joinNetwork,
+ leaveNetwork,
+ loadNetworks,
+ syncFromCore,
+ refresh,
+ subscribe,
+ requireNetworkId
+ };
+})();
diff --git a/app-shell/dist/peer-directory.js b/app-shell/dist/peer-directory.js
new file mode 100644
index 0000000..cff0511
--- /dev/null
+++ b/app-shell/dist/peer-directory.js
@@ -0,0 +1,266 @@
+window.BackOnePeerDirectory = (function () {
+ const NETWORK_KEY = 'backone.lastNetworkId';
+ const API = {
+ peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
+ networks: ['/api/backone/networks', '/app/api/networks', '/network'],
+ status: ['/api/backone/status', '/app/api/status', '/status'],
+ network: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ],
+ members: (id) => [
+ `/api/backone/network/${id}/members`,
+ `/app/api/network/${id}/members`,
+ `/unstable/controller/network/${id}/member`
+ ],
+ join: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ],
+ leave: (id) => [
+ `/api/backone/network/${id}`,
+ `/app/api/network/${id}`,
+ `/network/${id}`
+ ]
+ };
+
+ function apiGet(path) {
+ return BackOneAuth.apiGet(path);
+ }
+
+ /** Same data as `backone-cli -j peers` (GET /peer). */
+ async function loadCliPeers() {
+ try {
+ const peers = await BackOneAuth.apiGetFirst(API.peers);
+ return Array.isArray(peers) ? peers : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ /** Same data as `backone-cli -j listnetworks` (GET /network). */
+ async function loadNetworks() {
+ try {
+ const nets = await BackOneAuth.apiGetFirst(API.networks);
+ return Array.isArray(nets) ? nets : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ /** Same data as `backone-cli -j info` (GET /status). */
+ async function loadCoreStatus() {
+ try {
+ return await BackOneAuth.apiGetFirst(API.status);
+ } catch (_) {
+ try {
+ const res = await fetch('/health', { cache: 'no-store' });
+ return { online: res.ok };
+ } catch (_2) {
+ return { online: false };
+ }
+ }
+ }
+
+ function countLeafPeers(cliPeers) {
+ if (!Array.isArray(cliPeers)) return 0;
+ return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
+ }
+
+ async function loadNetwork(nwid) {
+ if (!nwid) return null;
+ const networks = await loadNetworks();
+ const found = networks.find(n => n.id === nwid);
+ if (found) return found;
+ try {
+ return await BackOneAuth.apiGetFirst(API.network(nwid));
+ } catch (_) {
+ return null;
+ }
+ }
+
+ function getLastNetworkId() {
+ return localStorage.getItem(NETWORK_KEY) || '';
+ }
+
+ function setLastNetworkId(nwid) {
+ if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
+ }
+
+ function peerIsOnline(p) {
+ if (!p) return false;
+ const paths = p.paths || [];
+ return paths.some(x => x.active && !x.expired);
+ }
+
+ function peerLinkLabel(p) {
+ if (!p) return '';
+ if (p.tunneled) return 'RELAY';
+ const paths = p.paths || [];
+ for (const path of paths) {
+ if (path.preferred && path.active && !path.expired) {
+ return 'DIRECT';
+ }
+ }
+ for (const path of paths) {
+ if (path.active && !path.expired) {
+ return 'DIRECT';
+ }
+ }
+ return 'RELAY';
+ }
+
+ function peerPreferredPath(p) {
+ if (!p) return '';
+ const paths = p.paths || [];
+ for (const path of paths) {
+ if (path.preferred && path.address) return path.address;
+ }
+ for (const path of paths) {
+ if (path.active && path.address) return path.address;
+ }
+ return '';
+ }
+
+ function cliPeerMap(cliPeers) {
+ const map = {};
+ for (const p of cliPeers) {
+ const id = (p.address || '').toLowerCase();
+ if (id) map[id] = p;
+ }
+ return map;
+ }
+
+ async function loadMemberMap(nwid) {
+ if (!nwid) return {};
+ try {
+ const data = await BackOneAuth.apiGetFirst(API.members(nwid));
+ const map = {};
+ if (data && Array.isArray(data.data)) {
+ for (const m of data.data) {
+ const id = (m.id || '').toLowerCase();
+ if (id) map[id] = m;
+ }
+ }
+ return map;
+ } catch (_) {
+ return {};
+ }
+ }
+
+ function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
+ const controllerName = member ? (member.name || '') : '';
+ const ips = member && member.ipAssignments ? member.ipAssignments : [];
+ return {
+ peerId,
+ networkId: networkId || '',
+ networkName: networkName || '',
+ displayName: BackOneContacts.getDisplayName(peerId, controllerName),
+ controllerName,
+ nickname: BackOneContacts.getNickname(peerId),
+ online: peerIsOnline(cliPeer),
+ latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
+ role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
+ version: (cliPeer && cliPeer.version) || '',
+ link: cliPeer ? peerLinkLabel(cliPeer) : '',
+ path: cliPeer ? peerPreferredPath(cliPeer) : '',
+ ip: ips.length ? ips[0] : '',
+ onNetwork: !!member
+ };
+ }
+
+ async function loadPeersForNetwork(nwid) {
+ if (!nwid) return [];
+
+ const [cliPeers, members, selfStatus, network] = await Promise.all([
+ loadCliPeers(),
+ loadMemberMap(nwid),
+ loadCoreStatus().catch(() => ({})),
+ loadNetwork(nwid)
+ ]);
+ await BackOneContacts.load();
+
+ const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
+ const networkId = nwid;
+ const networkName = network ? (network.name || network.id || nwid) : nwid;
+ const cliById = cliPeerMap(cliPeers);
+ const out = [];
+
+ if (Object.keys(members).length > 0) {
+ for (const peerId of Object.keys(members)) {
+ const member = members[peerId];
+ if (!member || member.authorized === false) continue;
+ if (peerId === selfId) continue;
+ out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
+ }
+ } else {
+ for (const peerId of Object.keys(cliById)) {
+ const p = cliById[peerId];
+ if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
+ if (p.role && p.role !== 'LEAF') continue;
+ out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
+ }
+ }
+
+ out.sort((a, b) => {
+ if (a.online !== b.online) return a.online ? -1 : 1;
+ return a.displayName.localeCompare(b.displayName);
+ });
+ return out;
+ }
+
+ function searchPeers(query, list) {
+ const q = (query || '').trim().toLowerCase();
+ if (!q) return list;
+ return list.filter(p =>
+ p.displayName.toLowerCase().includes(q) ||
+ p.peerId.includes(q) ||
+ (p.networkName || '').toLowerCase().includes(q) ||
+ (p.networkId || '').includes(q) ||
+ (p.controllerName || '').toLowerCase().includes(q) ||
+ (p.role || '').toLowerCase().includes(q) ||
+ (p.path || '').toLowerCase().includes(q) ||
+ (p.ip || '').toLowerCase().includes(q)
+ );
+ }
+
+ function normalizeNetworkId(input) {
+ const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
+ return id.length === 16 ? id : '';
+ }
+
+ async function joinNetwork(nwid) {
+ const id = normalizeNetworkId(nwid);
+ if (!id) {
+ throw new Error('Network ID must be 16 hexadecimal characters');
+ }
+ return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
+ }
+
+ async function leaveNetwork(nwid) {
+ const id = normalizeNetworkId(nwid);
+ if (!id) {
+ throw new Error('Network ID must be 16 hexadecimal characters');
+ }
+ return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
+ }
+
+ return {
+ loadCliPeers,
+ loadNetworks,
+ loadNetwork,
+ loadCoreStatus,
+ countLeafPeers,
+ loadPeersForNetwork,
+ getLastNetworkId,
+ setLastNetworkId,
+ joinNetwork,
+ leaveNetwork,
+ normalizeNetworkId,
+ searchPeers,
+ peerIsOnline,
+ peerLinkLabel
+ };
+})();
diff --git a/app-shell/dist/peer-picker.js b/app-shell/dist/peer-picker.js
new file mode 100644
index 0000000..dec84bc
--- /dev/null
+++ b/app-shell/dist/peer-picker.js
@@ -0,0 +1,193 @@
+window.BackOnePeerPicker = function PeerPicker(container, options) {
+ options = options || {};
+ const onSelect = options.onSelect || (() => {});
+ const onNetworkChange = options.onNetworkChange || (() => {});
+ const onAction = options.onAction || null;
+ const actionLabel = options.actionLabel || 'Select';
+ const showActionButton = options.showActionButton !== false;
+ const useSharedNetwork = options.useSharedNetwork !== false;
+ const layout = options.layout || 'default';
+
+ let selectedPeer = null;
+ let peers = [];
+ let currentNetwork = null;
+ let unsub = null;
+
+ if (layout === 'sidebar') {
+ container.innerHTML = `
+ `;
+ } else {
+ container.innerHTML = `
+
+
BackOne core · same network as backone-cli
+
+
Search
+
+
+
+
+ ${showActionButton ? `
${actionLabel} ` : ''}
+
`;
+ }
+
+ const netBanner = container.querySelector('#pp-network-banner');
+ const searchInput = container.querySelector('#pp-search');
+ const listEl = container.querySelector('#pp-list');
+ const selectedEl = container.querySelector('#pp-selected');
+ const actionBtn = container.querySelector('#pp-action');
+
+ function networkLabel(n) {
+ if (!n) return '';
+ return n.name ? `${n.name} (${n.id})` : n.id;
+ }
+
+ function renderNetworkBanner() {
+ if (!useSharedNetwork) return;
+ const id = BackOneNetworkSession.getNetworkId();
+ const core = BackOneNetworkSession.getCoreStatus();
+ if (!core.online) {
+ netBanner.innerHTML = 'BackOne offline — peers unavailable ';
+ return;
+ }
+ if (!currentNetwork || !id) {
+ netBanner.innerHTML = 'No network — join one in the header bar ';
+ return;
+ }
+ const labelSuffix = `${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
+ if (layout === 'sidebar') {
+ netBanner.textContent = `${networkLabel(currentNetwork)} · ${labelSuffix}`;
+ } else {
+ netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${textSuffix}`;
+ }
+ }
+
+ function renderList() {
+ const q = searchInput.value;
+ const filtered = BackOnePeerDirectory.searchPeers(q, peers);
+ const netTag = currentNetwork ? (currentNetwork.name || currentNetwork.id) : '';
+ listEl.innerHTML = filtered.map(p => {
+ const lat = p.latency != null ? `${p.latency}ms` : '—';
+ const link = p.link || 'DIRECT';
+ const meta = [p.role || 'LEAF', lat, link, netTag].filter(Boolean).join(' · ');
+ return `
+
+
+
+ ${escapeHtml(p.displayName || p.peerId)}
+ ${escapeHtml(meta)}
+
+ ✎
+ `;
+ }).join('') || 'No peers on this network ';
+
+ listEl.querySelectorAll('.peer-item').forEach(li => {
+ li.addEventListener('click', (ev) => {
+ if (ev.target.classList.contains('peer-edit')) return;
+ const id = li.dataset.id;
+ selectedPeer = peers.find(x => x.peerId === id) || null;
+ if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
+ renderList();
+ renderSelected();
+ if (selectedPeer) onSelect(selectedPeer, currentNetwork);
+ });
+ const editBtn = li.querySelector('.peer-edit');
+ if (editBtn) {
+ editBtn.addEventListener('click', async (ev) => {
+ ev.stopPropagation();
+ const id = li.dataset.id;
+ const peer = peers.find(x => x.peerId === id);
+ if (!peer) return;
+ const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
+ if (nick === null) return;
+ await BackOneContacts.setNickname(peer.peerId, nick);
+ BackOneContacts.invalidate();
+ await refreshPeers();
+ });
+ }
+ });
+ }
+
+ function renderSelected() {
+ if (layout === 'sidebar') return;
+ if (!selectedPeer) {
+ if (selectedEl) selectedEl.textContent = '';
+ if (actionBtn) actionBtn.disabled = true;
+ return;
+ }
+ const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
+ if (selectedEl) {
+ selectedEl.innerHTML = `Selected: ${escapeHtml(selectedPeer.displayName)} (${escapeHtml(selectedPeer.peerId)})${net ? ` on ${escapeHtml(net)} ` : ''}`;
+ }
+ if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
+ }
+
+ function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+ }
+
+ async function refreshPeers() {
+ currentNetwork = BackOneNetworkSession.getActiveNetwork();
+ if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
+ currentNetwork = await BackOneNetworkSession.refresh();
+ }
+ renderNetworkBanner();
+ const nwid = BackOneNetworkSession.getNetworkId();
+ await BackOneContacts.load();
+ peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
+ const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
+ if (lastPeerId) {
+ selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
+ }
+ renderList();
+ renderSelected();
+ onNetworkChange(currentNetwork, peers);
+ }
+
+ searchInput.addEventListener('input', renderList);
+ if (actionBtn) {
+ actionBtn.addEventListener('click', () => {
+ if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
+ });
+ }
+
+ if (useSharedNetwork) {
+ unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
+ }
+
+ refreshPeers();
+
+ return {
+ getSelectedPeer: () => selectedPeer,
+ getSelectedNetwork: () => currentNetwork,
+ getPeers: () => peers,
+ refresh: refreshPeers,
+ destroy: () => { if (unsub) unsub(); }
+ };
+};
+
+window.BackOneCommLayout = {
+ connectionBarHtml(peer, network) {
+ const netLabel = network
+ ? (network.name ? `${network.name} (${network.id})` : network.id)
+ : BackOneNetworkSession.getNetworkId() || '—';
+ const lat = peer && peer.latency != null ? `${peer.latency}ms` : '—';
+ const latClass = peer && peer.latency != null && peer.latency < 100 ? 'latency-ok' : '';
+ return `
+ Network ID ${escapeHtml(netLabel)}
+ Peer ID ${peer ? escapeHtml(peer.peerId) : '—'}
+ Role ${peer ? escapeHtml(peer.role || 'LEAF') : '—'}
+ Transport ${peer ? escapeHtml(peer.link || 'DIRECT') : '—'}
+ Latency ${lat}
+ Quality
`;
+ }
+};
+
+function escapeHtml(s) {
+ return String(s).replace(/&/g, '&').replace(//g, '>');
+}
diff --git a/app-shell/dist/styles.css b/app-shell/dist/styles.css
new file mode 100644
index 0000000..6f22c67
--- /dev/null
+++ b/app-shell/dist/styles.css
@@ -0,0 +1,944 @@
+:root {
+ --bg: #0f1419;
+ --surface: #1a2332;
+ --surface-2: #243044;
+ --border: #2d3a4d;
+ --text: #e7ecf1;
+ --muted: #8b9bb4;
+ --accent: #4a90d9;
+ --accent-dim: #2d5a8a;
+ --ok: #2ecc71;
+ --warn: #e6a23c;
+ --danger: #e74c3c;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ font-family: system-ui, -apple-system, sans-serif;
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.app-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.65rem 1rem;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ flex-wrap: wrap;
+}
+
+.app-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+}
+
+.app-brand h1 {
+ margin: 0;
+ font-size: 1.15rem;
+ font-weight: 600;
+}
+
+.core-badge {
+ font-size: 0.75rem;
+ padding: 0.15rem 0.55rem;
+ border-radius: 999px;
+ font-weight: 500;
+}
+
+.core-badge.online {
+ background: rgba(46, 204, 113, 0.15);
+ color: var(--ok);
+ border: 1px solid rgba(46, 204, 113, 0.35);
+}
+
+.core-badge.offline {
+ background: rgba(231, 76, 60, 0.12);
+ color: var(--danger);
+ border: 1px solid rgba(231, 76, 60, 0.3);
+}
+
+.network-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ flex-wrap: wrap;
+ flex: 1;
+ min-width: 240px;
+}
+
+.network-bar-label { font-size: 0.82rem; color: var(--muted); }
+
+.network-bar select,
+.network-bar input,
+.auth-token-bar input {
+ background: var(--bg);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0.35rem 0.5rem;
+ font-size: 0.85rem;
+}
+
+.network-bar input { width: 9.5rem; }
+
+.network-bar button,
+.auth-token-bar button {
+ background: var(--accent-dim);
+ color: #fff;
+ border: none;
+ padding: 0.35rem 0.65rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.network-bar-status {
+ font-size: 0.75rem;
+ color: #6b8cae;
+ flex: 1 1 100%;
+}
+
+.auth-token-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex: 1 1 100%;
+}
+
+.auth-token-hidden { display: none; }
+
+.auth-token-label { font-size: 0.82rem; color: var(--muted); }
+
+.auth-token-bar input { width: 14rem; }
+
+.addon-nav {
+ display: flex;
+ gap: 0.35rem;
+ margin-left: auto;
+}
+
+.addon-nav button {
+ background: transparent;
+ color: var(--muted);
+ border: 1px solid transparent;
+ padding: 0.4rem 0.85rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.88rem;
+}
+
+.addon-nav button:hover { color: var(--text); background: var(--surface-2); }
+
+.addon-nav button.active {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+}
+
+.addon-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.addon-panel > p {
+ padding: 1.5rem;
+ margin: 0;
+}
+
+/* —— comm layout (voice / video / chat) —— */
+
+.comm-layout {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.comm-sidebar {
+ width: 300px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--border);
+ background: var(--bg);
+ min-height: 0;
+}
+
+/* Chat & voice: 25% contact list / 75% main panel */
+.comm-layout.comm-split-layout .comm-sidebar {
+ width: 25%;
+ max-width: 25%;
+ flex: 0 0 25%;
+ min-width: 200px;
+}
+
+.comm-layout.comm-split-layout .comm-main {
+ flex: 1 1 75%;
+ width: 75%;
+ max-width: 75%;
+ min-width: 0;
+}
+
+.comm-layout.comm-split-layout .comm-sidebar-head {
+ padding: 0.75rem 0.75rem 0.4rem;
+}
+
+.comm-layout.comm-split-layout .comm-sidebar-head h2 {
+ font-size: 0.95rem;
+}
+
+.comm-layout.comm-split-layout .comm-sidebar-body {
+ padding: 0.5rem 0.65rem;
+ overflow: hidden;
+}
+
+.comm-layout.comm-split-layout .comm-sidebar-actions {
+ padding: 0.5rem 0.65rem 0.75rem;
+}
+
+.comm-sidebar-head {
+ padding: 1rem 1rem 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.comm-sidebar-head h2 {
+ margin: 0 0 0.35rem;
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.comm-sidebar-sub {
+ margin: 0;
+ font-size: 0.72rem;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ line-height: 1.4;
+}
+
+.comm-sidebar-body {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 0.75rem 1rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.comm-sidebar-actions {
+ padding: 0.75rem 1rem 1rem;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.comm-selected-label {
+ font-size: 0.78rem;
+ color: var(--muted);
+ margin-bottom: 0.15rem;
+}
+
+.comm-selected-label strong {
+ color: var(--text);
+ font-family: ui-monospace, monospace;
+}
+
+.comm-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ background: #0a0e14;
+}
+
+.comm-main-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.65rem 1rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
+ font-size: 0.85rem;
+}
+
+.comm-breadcrumb {
+ color: var(--muted);
+}
+
+.comm-breadcrumb strong { color: var(--text); }
+
+.comm-head-status {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ color: var(--muted);
+ font-size: 0.82rem;
+}
+
+.comm-head-status .connected {
+ color: var(--ok);
+ font-weight: 500;
+}
+
+.comm-stage {
+ flex: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+ background: #000;
+}
+
+.comm-stage video {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ background: #000;
+}
+
+.comm-remote-placeholder {
+ text-align: center;
+ color: var(--muted);
+}
+
+.comm-remote-placeholder .avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: 50%;
+ background: var(--surface-2);
+ margin: 0 auto 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ color: #5a6a7d;
+}
+
+.comm-remote-placeholder .peer-id {
+ font-family: ui-monospace, monospace;
+ color: var(--text);
+ font-size: 0.95rem;
+}
+
+.comm-remote-tag {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ background: rgba(0, 0, 0, 0.55);
+ padding: 0.2rem 0.55rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.comm-remote-tag .dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--accent);
+}
+
+.comm-call-controls {
+ position: absolute;
+ bottom: 1.25rem;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.45rem;
+ background: rgba(26, 35, 50, 0.92);
+ border-radius: 10px;
+ border: 1px solid var(--border);
+}
+
+.comm-call-controls button {
+ background: var(--surface-2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 0.45rem 0.85rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.82rem;
+}
+
+.comm-call-controls button.danger {
+ background: var(--danger);
+ border-color: var(--danger);
+ color: #fff;
+}
+
+.comm-connection-bar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.25rem 2rem;
+ padding: 0.65rem 1rem;
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ font-size: 0.78rem;
+}
+
+.comm-stat label {
+ display: block;
+ color: var(--muted);
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ margin-bottom: 0.15rem;
+}
+
+.comm-stat span {
+ font-family: ui-monospace, monospace;
+ font-size: 0.82rem;
+}
+
+.comm-stat .latency-ok { color: var(--ok); }
+
+.comm-quality {
+ display: inline-flex;
+ align-items: flex-end;
+ gap: 2px;
+ height: 14px;
+}
+
+.comm-quality span {
+ width: 3px;
+ background: var(--ok);
+ border-radius: 1px;
+}
+
+.comm-quality span:nth-child(1) { height: 4px; }
+.comm-quality span:nth-child(2) { height: 7px; }
+.comm-quality span:nth-child(3) { height: 10px; }
+.comm-quality span:nth-child(4) { height: 14px; }
+
+/* —— peer picker —— */
+
+.peer-picker { display: flex; flex-direction: column; flex: 1; min-height: 0; }
+
+.peer-info-box {
+ font-size: 0.82rem;
+ color: #9eb4d0;
+ margin-bottom: 0.75rem;
+ padding: 0.55rem 0.65rem;
+ background: rgba(45, 90, 138, 0.25);
+ border: 1px solid rgba(74, 144, 217, 0.25);
+ border-radius: 6px;
+ line-height: 1.35;
+}
+
+.peer-picker-row { display: block; margin-bottom: 0.65rem; }
+
+.peer-picker-row input {
+ width: 100%;
+ margin-top: 0.25rem;
+ padding: 0.45rem 0.55rem;
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-size: 0.85rem;
+}
+
+.peer-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ --peer-row-height: 3.25rem;
+ max-height: calc(var(--peer-row-height) * 10);
+ flex: 0 1 auto;
+ overflow-x: hidden;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ scrollbar-gutter: stable;
+}
+
+.peer-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ min-height: var(--peer-row-height);
+ padding: 0.55rem 0.65rem;
+ cursor: pointer;
+ border-bottom: 1px solid var(--border);
+ box-sizing: border-box;
+}
+
+.peer-item:last-child { border-bottom: none; }
+
+.peer-item:hover,
+.peer-item.active {
+ background: var(--surface-2);
+}
+
+.peer-item.active { border-left: 3px solid var(--accent); padding-left: calc(0.65rem - 3px); }
+
+.peer-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #666;
+ flex-shrink: 0;
+ margin-top: 0.35rem;
+}
+
+.peer-item.online .peer-dot { background: var(--ok); }
+
+.peer-main { flex: 1; min-width: 0; }
+
+.peer-name {
+ display: block;
+ font-family: ui-monospace, monospace;
+ font-size: 0.88rem;
+}
+
+.peer-sub {
+ color: var(--muted);
+ font-size: 0.72rem;
+ display: block;
+ margin-top: 0.15rem;
+}
+
+.peer-edit {
+ background: transparent;
+ border: none;
+ color: var(--muted);
+ cursor: pointer;
+ padding: 0.2rem;
+ font-size: 0.85rem;
+}
+
+.peer-empty { padding: 1rem; color: var(--muted); font-size: 0.85rem; }
+
+.peer-network-warn { color: var(--warn); }
+
+.btn-primary {
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ padding: 0.55rem 1rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.88rem;
+ font-weight: 500;
+}
+
+.btn-primary:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.btn-secondary {
+ background: var(--surface-2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 0.55rem 1rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.88rem;
+}
+
+/* —— chat —— */
+
+.chat-thread-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.comm-split-layout .chat-thread-panel {
+ padding: 0;
+}
+
+.chat-thread-header {
+ flex-shrink: 0;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border);
+ margin: 0;
+ font-size: 0.9rem;
+ background: var(--surface);
+}
+
+.chat-thread-header small {
+ color: var(--muted);
+ font-weight: normal;
+ margin-left: 0.35rem;
+}
+
+.chat-messages,
+#chat-thread {
+ flex: 1;
+ overflow: auto;
+ padding: 1rem;
+ margin: 0;
+ border: none;
+ border-radius: 0;
+ background: #0a0e14;
+}
+
+.chat-empty {
+ color: var(--muted);
+ text-align: center;
+ margin: 2rem 0;
+ font-size: 0.88rem;
+}
+
+.chat-message {
+ margin-bottom: 1rem;
+ max-width: 85%;
+}
+
+.chat-message-meta {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ margin-bottom: 0.25rem;
+}
+
+.chat-message-sender {
+ color: #e8a54b;
+ font-weight: 600;
+ font-size: 0.88rem;
+}
+
+.chat-message-time {
+ color: var(--muted);
+ font-size: 0.72rem;
+}
+
+.chat-message-body {
+ color: var(--text);
+ font-size: 0.9rem;
+ line-height: 1.45;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.chat-message-file {
+ margin-top: 0.35rem;
+ font-size: 0.85rem;
+ color: var(--accent);
+}
+
+.chat-compose {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border-top: 1px solid var(--border);
+ background: var(--surface);
+}
+
+.chat-compose textarea {
+ width: 100%;
+ padding: 0.5rem;
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ resize: vertical;
+}
+
+.chat-compose-row {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+/* —— voice —— */
+
+.voice-main-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+ background: #0a0e14;
+}
+
+.voice-active-banner {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.65rem 1rem;
+ background: rgba(45, 90, 138, 0.35);
+ border-bottom: 1px solid var(--border);
+ font-size: 0.88rem;
+}
+
+.voice-active-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--ok);
+ margin-right: 0.4rem;
+}
+
+.voice-active-controls {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.voice-active-controls button {
+ background: var(--surface-2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 0.35rem 0.75rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.82rem;
+}
+
+.voice-active-controls button.danger {
+ background: var(--danger);
+ border-color: var(--danger);
+ color: #fff;
+}
+
+.voice-session-hint {
+ margin: 1rem;
+ color: var(--text-muted);
+ font-size: 0.9rem;
+}
+
+.voice-remote-audio {
+ display: none;
+}
+
+.voice-history-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.voice-history-title {
+ margin: 0;
+ padding: 0.75rem 1rem;
+ font-size: 0.9rem;
+ font-weight: 600;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ background: var(--surface);
+}
+
+.voice-call-history {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ flex: 1;
+ overflow-y: auto;
+}
+
+.voice-call-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+}
+
+.voice-call-item:hover {
+ background: var(--surface-2);
+}
+
+.voice-call-icon {
+ width: 1.75rem;
+ text-align: center;
+ font-size: 1rem;
+ color: var(--accent);
+ flex-shrink: 0;
+}
+
+.voice-call-main {
+ flex: 1;
+ min-width: 0;
+}
+
+.voice-call-name {
+ display: block;
+ font-size: 0.9rem;
+ font-family: ui-monospace, monospace;
+}
+
+.voice-call-sub {
+ color: var(--muted);
+ font-size: 0.75rem;
+}
+
+.voice-call-when {
+ flex-shrink: 0;
+ color: var(--muted);
+ font-size: 0.75rem;
+}
+
+.voice-call-empty {
+ padding: 2rem 1rem;
+ text-align: center;
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+/* legacy voice stage (unused) */
+.voice-stage-inner {
+ text-align: center;
+ padding: 2rem;
+}
+
+.voice-stage-inner .avatar {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ background: var(--surface-2);
+ margin: 0 auto 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2.5rem;
+}
+
+/* —— ringing popup —— */
+
+.ringing-hidden { display: none; }
+
+#ringing-popup {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+#ringing-popup.ringing-hidden { display: none; }
+
+.ringing-card {
+ background: var(--surface);
+ padding: 2rem;
+ border-radius: 12px;
+ text-align: center;
+ min-width: 280px;
+}
+
+.ringing-card button {
+ margin: 0.5rem;
+ padding: 0.6rem 1.2rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.ringing-accept { background: var(--ok); color: #000; }
+.ringing-reject { background: var(--danger); color: #fff; }
+
+/* —— video call: remote fills main stage, local is PiP —— */
+
+.comm-layout.video-in-call .comm-sidebar {
+ display: none;
+}
+
+.comm-layout.video-in-call .comm-main-head {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 4;
+ background: linear-gradient(to bottom, rgba(10, 14, 20, 0.85), transparent);
+ border-bottom: none;
+}
+
+.video-stage .video-remote-main {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #000;
+}
+
+.video-stage .video-remote-stream {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0;
+ transition: opacity 0.25s ease;
+}
+
+.video-stage .video-remote-stream.active {
+ opacity: 1;
+}
+
+.video-stage.has-remote .comm-remote-placeholder {
+ display: none;
+}
+
+.video-stage .comm-remote-tag {
+ z-index: 3;
+}
+
+.video-local-pip {
+ position: absolute;
+ bottom: 4.5rem;
+ right: 1rem;
+ width: 180px;
+ z-index: 3;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ background: #000;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
+}
+
+.video-pip-label {
+ position: absolute;
+ top: 6px;
+ left: 8px;
+ z-index: 1;
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: rgba(255, 255, 255, 0.85);
+ background: rgba(0, 0, 0, 0.5);
+ padding: 0.1rem 0.35rem;
+ border-radius: 3px;
+}
+
+.video-local-pip video {
+ width: 100%;
+ height: auto;
+ display: block;
+ aspect-ratio: 4 / 3;
+ object-fit: cover;
+}
+
+.video-stage .comm-call-controls {
+ z-index: 4;
+}
diff --git a/app-shell/dist/webhub-client.js b/app-shell/dist/webhub-client.js
new file mode 100644
index 0000000..fc2f931
--- /dev/null
+++ b/app-shell/dist/webhub-client.js
@@ -0,0 +1,170 @@
+/**
+ * WebSocket client for backone-webhub (127.0.0.1, authtoken auth).
+ */
+window.BackOneWebHubClient = (function () {
+ let ws = null;
+ let connected = false;
+ let reconnectDelay = 1000;
+ let reconnectTimer = null;
+ let pollTimer = null;
+ let port = 9994;
+ let selfPeerId = '';
+ const seenIncoming = new Set();
+ const handlers = new Map();
+
+ function emit(type, msg) {
+ const set = handlers.get(type);
+ if (!set) return;
+ set.forEach(fn => {
+ try { fn(msg); } catch (_) { /* ignore */ }
+ });
+ const all = handlers.get('*');
+ if (all) {
+ all.forEach(fn => {
+ try { fn(type, msg); } catch (_) { /* ignore */ }
+ });
+ }
+ }
+
+ function subscribe(type, fn) {
+ if (!handlers.has(type)) handlers.set(type, new Set());
+ handlers.get(type).add(fn);
+ return () => handlers.get(type)?.delete(fn);
+ }
+
+ function send(msg) {
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(msg));
+ return true;
+ }
+ return false;
+ }
+
+ function registerPeer() {
+ const core = BackOneNetworkSession.getCoreStatus();
+ selfPeerId = ((core && core.address) || '').toLowerCase();
+ const networkId = BackOneNetworkSession.getNetworkId();
+ if (!selfPeerId) return;
+ send({ type: 'register', peerId: selfPeerId, networkId });
+ }
+
+ function stopHttpPoll() {
+ if (pollTimer) {
+ clearInterval(pollTimer);
+ pollTimer = null;
+ }
+ }
+
+ function startHttpPoll() {
+ stopHttpPoll();
+ pollTimer = setInterval(async () => {
+ if (connected) return;
+ const peerId = selfPeerId || ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
+ if (!peerId) return;
+ try {
+ const res = await BackOneAuth.fetchWithAuth(
+ `/controller/comms/call/incoming?peerId=${encodeURIComponent(peerId)}`
+ );
+ if (!res.ok) return;
+ const data = await res.json();
+ const calls = Array.isArray(data.calls) ? data.calls : [];
+ for (const call of calls) {
+ if (seenIncoming.has(call.callId)) continue;
+ seenIncoming.add(call.callId);
+ emit('incoming_call', {
+ type: 'incoming_call',
+ callId: call.callId,
+ callType: call.callType || 'voice',
+ callerId: call.callerId,
+ callerName: call.callerName || call.callerId,
+ networkId: call.networkId,
+ sdp: call.sdp
+ });
+ }
+ } catch (_) { /* ignore */ }
+ }, 2000);
+ }
+
+ function scheduleReconnect() {
+ if (reconnectTimer) return;
+ reconnectTimer = setTimeout(() => {
+ reconnectTimer = null;
+ connect();
+ }, reconnectDelay);
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
+ }
+
+ async function loadPort() {
+ try {
+ const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
+ if (res.ok) {
+ const cfg = await res.json();
+ if (cfg.webhubPort) port = cfg.webhubPort;
+ }
+ } catch (_) { /* ignore */ }
+ }
+
+ async function connect() {
+ await loadPort();
+ const token = await BackOneAuth.getToken();
+ if (!token) return false;
+
+ if (ws) {
+ try { ws.close(); } catch (_) { /* ignore */ }
+ ws = null;
+ }
+
+ return new Promise((resolve) => {
+ const url = `ws://127.0.0.1:${port}/?auth=${encodeURIComponent(token)}`;
+ try {
+ ws = new WebSocket(url);
+ } catch (_) {
+ connected = false;
+ startHttpPoll();
+ scheduleReconnect();
+ resolve(false);
+ return;
+ }
+
+ ws.onopen = () => {
+ connected = true;
+ reconnectDelay = 1000;
+ stopHttpPoll();
+ send({ type: 'auth', token });
+ registerPeer();
+ resolve(true);
+ };
+
+ ws.onmessage = (ev) => {
+ try {
+ const msg = JSON.parse(ev.data);
+ if (msg && msg.type) emit(msg.type, msg);
+ } catch (_) { /* ignore */ }
+ };
+
+ ws.onclose = () => {
+ connected = false;
+ ws = null;
+ startHttpPoll();
+ scheduleReconnect();
+ };
+
+ ws.onerror = () => {
+ connected = false;
+ };
+ });
+ }
+
+ BackOneNetworkSession.subscribe(() => {
+ if (connected) registerPeer();
+ });
+
+ return {
+ connect,
+ send,
+ subscribe,
+ isConnected: () => connected,
+ getSelfPeerId: () => selfPeerId,
+ registerPeer
+ };
+})();
diff --git a/app-shell/dist/webrtc-session.js b/app-shell/dist/webrtc-session.js
new file mode 100644
index 0000000..95b933e
--- /dev/null
+++ b/app-shell/dist/webrtc-session.js
@@ -0,0 +1,352 @@
+/**
+ * Shared WebRTC session — signaling via WebHub WS with HTTP fallback.
+ */
+window.BackOneWebRtcSession = (function () {
+ let config = {
+ icePreferOverlay: true,
+ iceFallbackTimeoutMs: 8000,
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
+ };
+ let pc = null;
+ let localStream = null;
+ let remoteStream = null;
+ let activeCall = null;
+ let iceFallbackTimer = null;
+ let httpIcePollTimer = null;
+ let httpAnswerPollTimer = null;
+ const listeners = new Set();
+
+ function emit(event, detail) {
+ listeners.forEach(fn => {
+ try { fn(event, detail); } catch (_) { /* ignore */ }
+ });
+ }
+
+ function subscribe(fn) {
+ listeners.add(fn);
+ return () => listeners.delete(fn);
+ }
+
+ function clearTimers() {
+ if (iceFallbackTimer) { clearTimeout(iceFallbackTimer); iceFallbackTimer = null; }
+ if (httpIcePollTimer) { clearInterval(httpIcePollTimer); httpIcePollTimer = null; }
+ if (httpAnswerPollTimer) { clearInterval(httpAnswerPollTimer); httpAnswerPollTimer = null; }
+ }
+
+ function overlayIceServers() {
+ return config.icePreferOverlay ? [] : (config.iceServers || []);
+ }
+
+ function fullIceServers() {
+ return config.iceServers || [];
+ }
+
+ function createPeerConnection(peer) {
+ clearTimers();
+ if (pc) {
+ try { pc.close(); } catch (_) { /* ignore */ }
+ }
+ pc = new RTCPeerConnection({ iceServers: overlayIceServers() });
+ remoteStream = new MediaStream();
+ pc.ontrack = (ev) => {
+ ev.streams[0].getTracks().forEach(t => remoteStream.addTrack(t));
+ emit('remote-track', { stream: remoteStream });
+ };
+ pc.onicecandidate = (ev) => {
+ if (!ev.candidate || !activeCall) return;
+ relayIce(activeCall.callId, {
+ candidate: ev.candidate.candidate,
+ sdpMid: ev.candidate.sdpMid,
+ sdpMLineIndex: ev.candidate.sdpMLineIndex
+ });
+ };
+ pc.onconnectionstatechange = () => {
+ emit('connection-state', { state: pc.connectionState });
+ if (pc.connectionState === 'connected') clearTimers();
+ };
+
+ if (config.icePreferOverlay) {
+ iceFallbackTimer = setTimeout(() => {
+ if (!pc || pc.connectionState === 'connected') return;
+ try {
+ pc.setConfiguration({ iceServers: fullIceServers() });
+ pc.restartIce();
+ } catch (_) { /* ignore */ }
+ }, config.iceFallbackTimeoutMs || 8000);
+ }
+ return pc;
+ }
+
+ async function relayIce(callId, candidate) {
+ if (BackOneWebHubClient.isConnected()) {
+ BackOneWebHubClient.send({
+ type: 'ice_candidate',
+ callId,
+ ...candidate
+ });
+ return;
+ }
+ const peerId = BackOneWebHubClient.getSelfPeerId()
+ || ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
+ await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/ice`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...candidate, fromPeerId: peerId })
+ }).catch(() => {});
+ }
+
+ function startHttpIcePoll(callId) {
+ let since = 0;
+ httpIcePollTimer = setInterval(async () => {
+ if (!pc || !activeCall || activeCall.callId !== callId) return;
+ const peerId = BackOneWebHubClient.getSelfPeerId()
+ || ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
+ try {
+ const res = await BackOneAuth.fetchWithAuth(
+ `/controller/comms/call/${encodeURIComponent(callId)}/ice?since=${since}&peerId=${encodeURIComponent(peerId)}`
+ );
+ if (!res.ok) return;
+ const data = await res.json();
+ const list = Array.isArray(data.candidates) ? data.candidates : [];
+ for (const c of list) {
+ try {
+ await pc.addIceCandidate({
+ candidate: c.candidate,
+ sdpMid: c.sdpMid,
+ sdpMLineIndex: c.sdpMLineIndex
+ });
+ since = Math.max(since, c.at || 0);
+ } catch (_) { /* ignore */ }
+ }
+ } catch (_) { /* ignore */ }
+ }, 1500);
+ }
+
+ function watchHubCallState() {
+ return BackOneWebHubClient.subscribe('call_state', async (msg) => {
+ if (!activeCall || msg.callId !== activeCall.callId) return;
+ if (msg.state === 'accepted' && msg.sdp && activeCall.role === 'caller') {
+ try {
+ await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
+ emit('call-accepted', activeCall);
+ } catch (_) { /* ignore */ }
+ }
+ if (msg.state === 'ended' || msg.state === 'rejected') {
+ hangUp(false);
+ }
+ });
+ }
+
+ let unsubHub = [];
+
+ async function waitForOutgoingCallId() {
+ return new Promise((resolve) => {
+ const unsub = BackOneWebHubClient.subscribe('call_state', (msg) => {
+ if (msg.callId && msg.state === 'ringing') {
+ unsub();
+ resolve(msg.callId);
+ }
+ });
+ setTimeout(() => { unsub(); resolve(null); }, 5000);
+ });
+ }
+
+ async function init() {
+ try {
+ const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
+ if (res.ok) {
+ const cfg = await res.json();
+ config = { ...config, ...cfg };
+ }
+ } catch (_) { /* ignore */ }
+
+ unsubHub.forEach(fn => fn && fn());
+ unsubHub = [
+ watchHubCallState(),
+ BackOneWebHubClient.subscribe('ice_candidate', async (msg) => {
+ if (!pc || !activeCall || msg.callId !== activeCall.callId) return;
+ try {
+ await pc.addIceCandidate({
+ candidate: msg.candidate,
+ sdpMid: msg.sdpMid,
+ sdpMLineIndex: msg.sdpMLineIndex
+ });
+ } catch (_) { /* ignore */ }
+ })
+ ];
+ }
+
+ async function getMedia(callType) {
+ const audio = true;
+ const video = callType === 'video';
+ localStream = await navigator.mediaDevices.getUserMedia({ audio, video });
+ return localStream;
+ }
+
+ function attachLocalTracks() {
+ if (!pc || !localStream) return;
+ localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
+ }
+
+ async function startOutgoingCall(peer, callType) {
+ if (!peer || activeCall) throw new Error('Already in a call');
+ const networkId = BackOneNetworkSession.getNetworkId();
+ const selfId = ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
+ createPeerConnection(peer);
+ localStream = await getMedia(callType);
+ attachLocalTracks();
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+
+ const payload = {
+ type: 'call_offer',
+ callType,
+ networkId,
+ targetId: peer.peerId,
+ callerName: peer.displayName || peer.peerId,
+ sdp: offer.sdp,
+ overlayIps: peer.ip ? [peer.ip] : []
+ };
+
+ let callId = null;
+ if (BackOneWebHubClient.isConnected()) {
+ BackOneWebHubClient.send(payload);
+ callId = await waitForOutgoingCallId();
+ if (!callId) throw new Error('Call signaling timeout');
+ } else {
+ const res = await BackOneAuth.fetchWithAuth('/controller/comms/call', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ callType,
+ type: callType,
+ networkId,
+ targetId: peer.peerId,
+ callerId: selfId,
+ callerName: peer.displayName || peer.peerId,
+ sdp: offer.sdp
+ })
+ });
+ if (!res.ok) throw new Error('Call signaling failed');
+ const data = await res.json();
+ callId = data.callId;
+ startHttpAnswerPoll(callId);
+ }
+
+ activeCall = {
+ callId,
+ role: 'caller',
+ callType,
+ peer,
+ networkId
+ };
+ if (!BackOneWebHubClient.isConnected()) startHttpIcePoll(activeCall.callId);
+ emit('call-started', activeCall);
+ return activeCall;
+ }
+
+ function startHttpAnswerPoll(callId) {
+ httpAnswerPollTimer = setInterval(async () => {
+ if (!pc || !activeCall || activeCall.callId !== callId) return;
+ try {
+ const res = await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}`);
+ if (!res.ok) return;
+ const data = await res.json();
+ if (data.state === 'accepted' && data.answerSdp) {
+ await pc.setRemoteDescription({ type: 'answer', sdp: data.answerSdp });
+ clearInterval(httpAnswerPollTimer);
+ httpAnswerPollTimer = null;
+ emit('call-accepted', activeCall);
+ }
+ } catch (_) { /* ignore */ }
+ }, 1500);
+ }
+
+ async function acceptIncomingCall(incoming) {
+ if (!incoming || activeCall) throw new Error('Already in a call');
+ const callType = incoming.callType || 'voice';
+ const callId = incoming.callId;
+ createPeerConnection({ peerId: incoming.callerId });
+ localStream = await getMedia(callType);
+ attachLocalTracks();
+
+ await pc.setRemoteDescription({ type: 'offer', sdp: incoming.sdp });
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ if (BackOneWebHubClient.isConnected()) {
+ BackOneWebHubClient.send({
+ type: 'call_answer',
+ callId,
+ sdp: answer.sdp
+ });
+ } else {
+ await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/answer`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ sdp: answer.sdp })
+ });
+ startHttpIcePoll(callId);
+ }
+
+ activeCall = {
+ callId,
+ role: 'callee',
+ callType,
+ peer: { peerId: incoming.callerId, displayName: incoming.callerName },
+ networkId: incoming.networkId
+ };
+ emit('call-started', activeCall);
+ return activeCall;
+ }
+
+ async function rejectIncomingCall(callId) {
+ if (BackOneWebHubClient.isConnected()) {
+ BackOneWebHubClient.send({ type: 'call_reject', callId });
+ } else {
+ await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/reject`, {
+ method: 'POST'
+ }).catch(() => {});
+ }
+ }
+
+ async function hangUp(signalRemote = true) {
+ const callId = activeCall?.callId;
+ clearTimers();
+ if (signalRemote && callId) {
+ if (BackOneWebHubClient.isConnected()) {
+ BackOneWebHubClient.send({ type: 'call_end', callId });
+ } else {
+ await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/end`, {
+ method: 'POST'
+ }).catch(() => {});
+ }
+ }
+ if (pc) {
+ try { pc.close(); } catch (_) { /* ignore */ }
+ pc = null;
+ }
+ if (localStream) {
+ localStream.getTracks().forEach(t => t.stop());
+ localStream = null;
+ }
+ remoteStream = null;
+ activeCall = null;
+ emit('call-ended', {});
+ }
+
+ return {
+ init,
+ subscribe,
+ startOutgoingCall,
+ acceptIncomingCall,
+ rejectIncomingCall,
+ hangUp,
+ getPeerConnection: () => pc,
+ getLocalStream: () => localStream,
+ getRemoteStream: () => remoteStream,
+ getActiveCall: () => activeCall,
+ isInCall: () => !!activeCall
+ };
+})();
diff --git a/app-tray/addon.json b/app-tray/addon.json
new file mode 100644
index 0000000..1e9a5bf
--- /dev/null
+++ b/app-tray/addon.json
@@ -0,0 +1,6 @@
+{
+ "id": "app-tray",
+ "name": "BackOne Tray",
+ "version": "1.17.0",
+ "requires": []
+}
diff --git a/app-tray/backone-tray-autostart.desktop b/app-tray/backone-tray-autostart.desktop
new file mode 100644
index 0000000..59ed475
--- /dev/null
+++ b/app-tray/backone-tray-autostart.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Name=BackOne Tray
+Comment=Start BackOne tray icon at login
+Exec=/usr/bin/backone-tray
+Icon=network-workgroup
+Terminal=false
+X-GNOME-Autostart-enabled=true
+Hidden=false
diff --git a/app-tray/backone-tray.desktop b/app-tray/backone-tray.desktop
new file mode 100644
index 0000000..d31939c
--- /dev/null
+++ b/app-tray/backone-tray.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Type=Application
+Name=BackOne Tray
+Comment=BackOne system tray for voice, video, and chat
+Exec=/usr/bin/backone-tray
+Icon=network-workgroup
+Terminal=false
+Categories=Network;
diff --git a/app-tray/backone-tray.py b/app-tray/backone-tray.py
new file mode 100755
index 0000000..8b3849d
--- /dev/null
+++ b/app-tray/backone-tray.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+"""BackOne system tray — opens /app UI and addon deep links."""
+import os
+import signal
+import subprocess
+import sys
+import threading
+import time
+import urllib.request
+
+import gi
+gi.require_version('Gtk', '3.0')
+try:
+ gi.require_version('AyatanaAppIndicator3', '0.1')
+ from gi.repository import AyatanaAppIndicator3 as AppIndicator3
+except (ImportError, ValueError):
+ gi.require_version('AppIndicator3', '0.1')
+ from gi.repository import AppIndicator3
+
+from gi.repository import GLib, Gtk
+
+APP_URL = os.environ.get('BACKONE_APP_URL', 'http://127.0.0.1:9993/app/')
+HEALTH_URL = os.environ.get('BACKONE_HEALTH_URL', 'http://127.0.0.1:9993/health')
+ICON_NAME = 'network-workgroup'
+SERVICE_WAIT_SECONDS = int(os.environ.get('BACKONE_SERVICE_WAIT', '45'))
+
+
+class BackOneTray:
+ def __init__(self):
+ self._windows = []
+ self._service_ready = False
+ self.indicator = AppIndicator3.Indicator.new(
+ 'backone-tray',
+ ICON_NAME,
+ AppIndicator3.IndicatorCategory.APPLICATION_STATUS,
+ )
+ self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
+ self.indicator.set_menu(self._build_menu())
+ try:
+ self.indicator.connect('activate', lambda *_: self._open_url(APP_URL))
+ except TypeError:
+ pass
+ self._update_status()
+ GLib.timeout_add_seconds(30, self._on_timer)
+ GLib.idle_add(self._bootstrap_service)
+
+ def _build_menu(self):
+ menu = Gtk.Menu()
+
+ def add_item(label, callback):
+ item = Gtk.MenuItem(label=label)
+ item.connect('activate', lambda *_: callback())
+ item.show()
+ menu.append(item)
+ return item
+
+ add_item('Open BackOne App', lambda: self._open_url(APP_URL))
+ add_item('Voice', lambda: self._open_url(APP_URL + '#/voice'))
+ add_item('Video', lambda: self._open_url(APP_URL + '#/video'))
+ add_item('Chat', lambda: self._open_url(APP_URL + '#/chat'))
+ menu.append(Gtk.SeparatorMenuItem())
+ add_item('Restart BackOne Service', self._restart_service)
+ add_item('Quit Tray', Gtk.main_quit)
+ menu.show()
+ return menu
+
+ def _service_ok(self):
+ try:
+ with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp:
+ return resp.status == 200
+ except Exception:
+ return False
+
+ def _systemd_active(self):
+ try:
+ result = subprocess.run(
+ ['systemctl', 'is-active', 'backone'],
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ return result.returncode == 0 and result.stdout.strip() == 'active'
+ except (OSError, subprocess.TimeoutExpired):
+ return False
+
+ def _start_service(self):
+ for cmd in (
+ ['systemctl', 'start', 'backone'],
+ ['pkexec', 'systemctl', 'start', 'backone'],
+ ):
+ try:
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ timeout=30,
+ )
+ if result.returncode == 0:
+ return True
+ except (OSError, subprocess.TimeoutExpired):
+ continue
+ return False
+
+ def _wait_for_service(self, timeout=SERVICE_WAIT_SECONDS):
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ if self._service_ok():
+ self._service_ready = True
+ return True
+ time.sleep(0.5)
+ return False
+
+ def _ensure_service(self):
+ if self._service_ok():
+ self._service_ready = True
+ return True
+ if not self._systemd_active():
+ self._start_service()
+ return self._wait_for_service()
+
+ def _bootstrap_service(self):
+ def worker():
+ ready = self._ensure_service()
+ GLib.idle_add(self._on_bootstrap_done, ready)
+ threading.Thread(target=worker, daemon=True).start()
+ return False
+
+ def _on_bootstrap_done(self, ready):
+ self._service_ready = ready
+ self._update_status()
+ return False
+
+ def _update_status(self):
+ online = self._service_ok()
+ self._service_ready = online
+ title = 'BackOne (online)' if online else 'BackOne (offline)'
+ self.indicator.set_title(title)
+
+ def _on_timer(self):
+ self._update_status()
+ return True
+
+ def _show_offline_dialog(self):
+ dialog = Gtk.MessageDialog(
+ transient_for=None,
+ flags=0,
+ message_type=Gtk.MessageType.ERROR,
+ buttons=Gtk.ButtonsType.OK,
+ text='BackOne service is not running',
+ )
+ dialog.format_secondary_text(
+ 'Could not reach the BackOne web server on 127.0.0.1:9993.\n\n'
+ 'Use "Restart BackOne Service" from the tray menu, or run:\n'
+ ' sudo systemctl start backone'
+ )
+ dialog.run()
+ dialog.destroy()
+
+ def _open_url(self, url):
+ if not self._ensure_service():
+ self._show_offline_dialog()
+ return
+ if self._try_webkit(url):
+ return
+ for cmd in (
+ ['xdg-open', url],
+ ['gio', 'open', url],
+ ['sensible-browser', url],
+ ):
+ try:
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ return
+ except OSError:
+ continue
+
+ def _try_webkit(self, url):
+ try:
+ gi.require_version('WebKit2', '4.1')
+ except ValueError:
+ try:
+ gi.require_version('WebKit2', '4.0')
+ except ValueError:
+ return False
+ from gi.repository import WebKit2
+
+ win = Gtk.Window(title='BackOne')
+ win.set_default_size(900, 640)
+ view = WebKit2.WebView()
+ view.load_uri(url)
+ win.add(view)
+ win.connect('destroy', lambda w: self._windows.remove(w) if w in self._windows else None)
+ self._windows.append(win)
+ win.show_all()
+ return True
+
+ def _restart_service(self):
+ subprocess.Popen(
+ ['pkexec', 'systemctl', 'restart', 'backone'],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ def worker():
+ ready = self._wait_for_service(timeout=30)
+ GLib.idle_add(self._on_bootstrap_done, ready)
+ threading.Thread(target=worker, daemon=True).start()
+
+
+def main():
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ BackOneTray()
+ Gtk.main()
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/backone.spec b/backone.spec
index 532dfc5..73e979f 100644
--- a/backone.spec
+++ b/backone.spec
@@ -1,5 +1,5 @@
Name: backone
-Version: 1.14.1
+Version: 1.17.1
Release: 1%{?dist}
Summary: BackOne global ethernet switch
@@ -155,6 +155,15 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/backone
%endif
%changelog
+* Tue Jun 30 2026 Dedy Sutanto - 1.17.1
+- Fix /api/backone routes and reinstallable Web UI packages
+
+* Tue Jun 30 2026 Dedy Sutanto - 1.17.0
+- Tray app, auto web server, peer contact UI
+
+* Tue Jun 30 2026 Dedy Sutanto - 1.16.0
+- Release notes
+
* Tue Oct 8 2024 Dedy Sutanto - 1.14.1
- see https://github.com/proitlab/BackOne for release notes
diff --git a/ci/scripts/bump_version.sh b/ci/scripts/bump_version.sh
new file mode 100755
index 0000000..796b459
--- /dev/null
+++ b/ci/scripts/bump_version.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -euo pipefail
+
+if [ $# -lt 1 ]; then
+ echo "Usage: $0 [changelog message]" >&2
+ exit 1
+fi
+
+VERSION="$1"
+MESSAGE="${2:-see https://github.com/proitlab/BackOne for release notes}"
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+IFS='.' read -r MAJ MIN REV <<< "${VERSION%%-*}"
+REV="${REV:-0}"
+
+sed -i "s/^#define ZEROTIER_ONE_VERSION_MAJOR .*/#define ZEROTIER_ONE_VERSION_MAJOR ${MAJ}/" "$ROOT/version.h"
+sed -i "s/^#define ZEROTIER_ONE_VERSION_MINOR .*/#define ZEROTIER_ONE_VERSION_MINOR ${MIN}/" "$ROOT/version.h"
+sed -i "s/^#define ZEROTIER_ONE_VERSION_REVISION .*/#define ZEROTIER_ONE_VERSION_REVISION ${REV}/" "$ROOT/version.h"
+sed -i "s/^#define ZEROTIER_ONE_VERSION_BUILD .*/#define ZEROTIER_ONE_VERSION_BUILD 0/" "$ROOT/version.h"
+
+echo "$VERSION" > "$ROOT/debian/version"
+
+"$ROOT/ci/scripts/munge_debian_changelog.sh" "$ROOT/debian/changelog" "$VERSION" \
+ "Dedy Sutanto " "$MESSAGE"
+
+"$ROOT/ci/scripts/munge_rpm_spec.sh" "$ROOT/backone.spec" "$VERSION" \
+ "Dedy Sutanto " "$MESSAGE"
+
+for addon in comms-core webhub voice video chat secure-chat updater app-tray; do
+ if [ -f "$ROOT/addons/$addon/addon.json" ]; then
+ sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" "$ROOT/addons/$addon/addon.json"
+ fi
+done
+
+echo "Bumped to ${VERSION}"
diff --git a/ci/scripts/munge_debian_changelog.sh b/ci/scripts/munge_debian_changelog.sh
index d37cb06..2f8dd32 100755
--- a/ci/scripts/munge_debian_changelog.sh
+++ b/ci/scripts/munge_debian_changelog.sh
@@ -31,7 +31,7 @@ else
fi
awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
- 'BEGIN{print "zerotier-one (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \
+ 'BEGIN{print "backone (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \
${FILE} > ${FILE}.new
mv ${FILE}.new ${FILE}
diff --git a/ci/scripts/sync-authtoken.sh b/ci/scripts/sync-authtoken.sh
new file mode 100644
index 0000000..13232e9
--- /dev/null
+++ b/ci/scripts/sync-authtoken.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
+set -e
+
+BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
+SECRET="${BACKONE_HOME}/authtoken.secret"
+
+trim_token() {
+ tr -d '\n\r \t' < "$1"
+}
+
+sync_for_user() {
+ user="$1"
+ home="$2"
+ [ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
+ user_token="${home}/.backOneOneAuthToken"
+
+ if [ -f "$user_token" ] && [ -s "$user_token" ]; then
+ ut="$(trim_token "$user_token")"
+ [ -n "$ut" ] || return 0
+ st=""
+ if [ -f "$SECRET" ]; then
+ st="$(trim_token "$SECRET" 2>/dev/null || true)"
+ fi
+ if [ "$ut" != "$st" ]; then
+ printf '%s' "$ut" > "$SECRET"
+ chown backone:backone "$SECRET" 2>/dev/null || true
+ chmod 0600 "$SECRET" 2>/dev/null || true
+ echo "Synced ${SECRET} from ${user_token}"
+ fi
+ return 0
+ fi
+
+ if [ -f "$SECRET" ]; then
+ install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
+ || cp "$SECRET" "$user_token"
+ chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
+ chmod 0600 "$user_token" 2>/dev/null || true
+ echo "Installed ${user_token} from ${SECRET}"
+ fi
+}
+
+if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
+ home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
+ sync_for_user "$SUDO_USER" "$home"
+fi
+
+if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
+ home="$(getent passwd "$USER" | cut -d: -f6)"
+ sync_for_user "$USER" "$home"
+fi
+
+exit 0
diff --git a/controller/CommsController.cpp b/controller/CommsController.cpp
new file mode 100644
index 0000000..bee4938
--- /dev/null
+++ b/controller/CommsController.cpp
@@ -0,0 +1,405 @@
+/*
+ * Copyright (c)2019 ZeroTier, Inc.
+ *
+ * Use of this software is governed by the Business Source License included
+ * in the LICENSE.TXT file in the project's root directory.
+ */
+/****/
+
+#include "CommsController.hpp"
+
+#include "../node/Mutex.hpp"
+#include "../node/Utils.hpp"
+#include "../osdep/OSUtils.hpp"
+
+#include
+#include
+#include
+#include
+
+namespace ZeroTier {
+
+namespace {
+
+struct IceEntry
+{
+ std::string fromPeerId;
+ std::string candidate;
+ std::string sdpMid;
+ int sdpMLineIndex;
+ int64_t at;
+};
+
+struct CallState
+{
+ std::string callId;
+ std::string callType;
+ std::string networkId;
+ std::string callerId;
+ std::string calleeId;
+ std::string callerName;
+ std::string state;
+ std::string offerSdp;
+ std::string answerSdp;
+ int64_t createdAt;
+};
+
+Mutex g_commsMutex;
+std::map g_calls;
+std::map > g_iceByCall;
+std::map > g_callsByPeer;
+
+std::string lowerId(const std::string &id)
+{
+ std::string out = id;
+ for (std::string::iterator i(out.begin()); i != out.end(); ++i) {
+ if (*i >= 'A' && *i <= 'F') {
+ *i = (char)(*i - 'A' + 'a');
+ }
+ }
+ return out;
+}
+
+std::string newCallId()
+{
+ uint8_t randBytes[8];
+ Utils::getSecureRandom(randBytes, sizeof(randBytes));
+ char randHex[17];
+ Utils::hex(randBytes, (unsigned int)sizeof(randBytes), randHex);
+ return std::string("call_") + std::to_string(OSUtils::now()) + "_" + randHex;
+}
+
+void trackCallForPeer(const std::string &peerId, const std::string &callId)
+{
+ if (peerId.empty()) {
+ return;
+ }
+ g_callsByPeer[peerId].insert(callId);
+}
+
+void untrackCall(const CallState &call)
+{
+ const std::string caller = lowerId(call.callerId);
+ const std::string callee = lowerId(call.calleeId);
+ if (!caller.empty()) {
+ g_callsByPeer[caller].erase(call.callId);
+ if (g_callsByPeer[caller].empty()) {
+ g_callsByPeer.erase(caller);
+ }
+ }
+ if (!callee.empty()) {
+ g_callsByPeer[callee].erase(call.callId);
+ if (g_callsByPeer[callee].empty()) {
+ g_callsByPeer.erase(callee);
+ }
+ }
+}
+
+void purgeCall(const std::string &callId)
+{
+ std::map::iterator it = g_calls.find(callId);
+ if (it != g_calls.end()) {
+ untrackCall(it->second);
+ g_calls.erase(it);
+ }
+ g_iceByCall.erase(callId);
+}
+
+nlohmann::json callToIncomingJson(const CallState &call)
+{
+ nlohmann::json out = nlohmann::json::object();
+ out["callId"] = call.callId;
+ out["callType"] = call.callType;
+ out["networkId"] = call.networkId;
+ out["callerId"] = call.callerId;
+ out["callerName"] = call.callerName;
+ out["sdp"] = call.offerSdp;
+ out["state"] = call.state;
+ return out;
+}
+
+nlohmann::json webrtcConfigFromLocal(const nlohmann::json &localConfig)
+{
+ nlohmann::json out = nlohmann::json::object();
+ out["icePreferOverlay"] = true;
+ out["iceFallbackTimeoutMs"] = 8000;
+ out["webhubPort"] = 9994;
+ out["iceServers"] = nlohmann::json::array();
+ out["iceServers"].push_back(nlohmann::json::object({ { "urls", "stun:stun.l.google.com:19302" } }));
+
+ try {
+ if (localConfig.contains("settings") && localConfig["settings"].is_object()) {
+ const nlohmann::json &settings = localConfig["settings"];
+ if (settings.contains("icePreferOverlay") && settings["icePreferOverlay"].is_boolean()) {
+ out["icePreferOverlay"] = settings["icePreferOverlay"].get();
+ }
+ if (settings.contains("iceFallbackTimeoutMs") && settings["iceFallbackTimeoutMs"].is_number_integer()) {
+ out["iceFallbackTimeoutMs"] = settings["iceFallbackTimeoutMs"].get();
+ }
+ if (settings.contains("webhubPort") && settings["webhubPort"].is_number_integer()) {
+ out["webhubPort"] = settings["webhubPort"].get();
+ }
+ if (settings.contains("iceServers") && settings["iceServers"].is_array()) {
+ out["iceServers"] = settings["iceServers"];
+ }
+ }
+ } catch ( ... ) {
+ }
+ return out;
+}
+
+} // namespace
+
+void CommsController::configureRoutes(
+ httplib::Server &s,
+ httplib::Server &sv6,
+ const std::function &localConfigGetter,
+ const std::function &setContent)
+{
+ auto webrtcConfigGet = [localConfigGetter, setContent](const httplib::Request &req, httplib::Response &res) {
+ nlohmann::json localConfig = nlohmann::json::object();
+ try {
+ localConfig = localConfigGetter();
+ } catch ( ... ) {
+ }
+ setContent(req, res, webrtcConfigFromLocal(localConfig).dump());
+ };
+ s.Get("/app/api/webrtc-config", webrtcConfigGet);
+ sv6.Get("/app/api/webrtc-config", webrtcConfigGet);
+
+ auto callCreate = [setContent](const httplib::Request &req, httplib::Response &res) {
+ try {
+ nlohmann::json body = OSUtils::jsonParse(req.body);
+ if (!body.is_object()) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"body must be a JSON object\"}");
+ return;
+ }
+ const std::string targetId = lowerId(body.contains("targetId") && body["targetId"].is_string()
+ ? body["targetId"].get() : "");
+ const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
+ ? body["sdp"].get() : "";
+ if (targetId.empty() || sdp.empty()) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"targetId and sdp are required\"}");
+ return;
+ }
+ std::string callType = "voice";
+ if (body.contains("callType") && body["callType"].is_string()) {
+ callType = body["callType"].get();
+ } else if (body.contains("type") && body["type"].is_string()) {
+ const std::string t = body["type"].get();
+ if (t == "voice" || t == "video") {
+ callType = t;
+ }
+ }
+ const std::string networkId = body.contains("networkId") && body["networkId"].is_string()
+ ? lowerId(body["networkId"].get()) : "";
+ const std::string callerId = body.contains("callerId") && body["callerId"].is_string()
+ ? lowerId(body["callerId"].get()) : "local";
+ const std::string callerName = body.contains("callerName") && body["callerName"].is_string()
+ ? body["callerName"].get() : callerId;
+
+ CallState call;
+ call.callId = newCallId();
+ call.callType = callType;
+ call.networkId = networkId;
+ call.callerId = callerId;
+ call.calleeId = targetId;
+ call.callerName = callerName;
+ call.state = "ringing";
+ call.offerSdp = sdp;
+ call.createdAt = OSUtils::now();
+
+ Mutex::Lock _l(g_commsMutex);
+ g_calls[call.callId] = call;
+ trackCallForPeer(call.callerId, call.callId);
+ trackCallForPeer(call.calleeId, call.callId);
+
+ nlohmann::json out = nlohmann::json::object();
+ out["callId"] = call.callId;
+ out["state"] = call.state;
+ setContent(req, res, out.dump());
+ } catch ( ... ) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"invalid JSON\"}");
+ }
+ };
+ s.Post("/controller/comms/call", callCreate);
+ sv6.Post("/controller/comms/call", callCreate);
+
+ auto callIncoming = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
+ if (peerId.empty()) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"peerId query parameter is required\"}");
+ return;
+ }
+ nlohmann::json calls = nlohmann::json::array();
+ Mutex::Lock _l(g_commsMutex);
+ std::map >::const_iterator it = g_callsByPeer.find(peerId);
+ if (it != g_callsByPeer.end()) {
+ for (std::set::const_iterator c = it->second.begin(); c != it->second.end(); ++c) {
+ std::map::const_iterator callIt = g_calls.find(*c);
+ if (callIt == g_calls.end()) {
+ continue;
+ }
+ if (lowerId(callIt->second.calleeId) != peerId || callIt->second.state != "ringing") {
+ continue;
+ }
+ calls.push_back(callToIncomingJson(callIt->second));
+ }
+ }
+ nlohmann::json out = nlohmann::json::object();
+ out["calls"] = calls;
+ setContent(req, res, out.dump());
+ };
+ s.Get("/controller/comms/call/incoming", callIncoming);
+ sv6.Get("/controller/comms/call/incoming", callIncoming);
+
+ auto callAnswer = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ try {
+ nlohmann::json body = OSUtils::jsonParse(req.body);
+ const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
+ ? body["sdp"].get() : "";
+ if (sdp.empty()) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"sdp is required\"}");
+ return;
+ }
+ Mutex::Lock _l(g_commsMutex);
+ std::map::iterator it = g_calls.find(callId);
+ if (it == g_calls.end()) {
+ res.status = 404;
+ setContent(req, res, "{\"error\":\"call not found\"}");
+ return;
+ }
+ it->second.answerSdp = sdp;
+ it->second.state = "accepted";
+ nlohmann::json out = nlohmann::json::object();
+ out["callId"] = callId;
+ out["state"] = "accepted";
+ out["sdp"] = sdp;
+ setContent(req, res, out.dump());
+ } catch ( ... ) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"invalid JSON\"}");
+ }
+ };
+ s.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
+ sv6.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
+
+ auto callReject = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ Mutex::Lock _l(g_commsMutex);
+ purgeCall(callId);
+ nlohmann::json out = nlohmann::json::object();
+ out["callId"] = callId;
+ out["state"] = "rejected";
+ setContent(req, res, out.dump());
+ };
+ s.Post("/controller/comms/call/([^/]+)/reject", callReject);
+ sv6.Post("/controller/comms/call/([^/]+)/reject", callReject);
+
+ auto callEnd = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ Mutex::Lock _l(g_commsMutex);
+ purgeCall(callId);
+ nlohmann::json out = nlohmann::json::object();
+ out["callId"] = callId;
+ out["state"] = "ended";
+ setContent(req, res, out.dump());
+ };
+ s.Post("/controller/comms/call/([^/]+)/end", callEnd);
+ sv6.Post("/controller/comms/call/([^/]+)/end", callEnd);
+
+ auto callIcePost = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ try {
+ nlohmann::json body = OSUtils::jsonParse(req.body);
+ IceEntry entry;
+ entry.fromPeerId = body.contains("fromPeerId") && body["fromPeerId"].is_string()
+ ? lowerId(body["fromPeerId"].get()) : "";
+ entry.candidate = body.contains("candidate") && body["candidate"].is_string()
+ ? body["candidate"].get() : "";
+ entry.sdpMid = body.contains("sdpMid") && body["sdpMid"].is_string()
+ ? body["sdpMid"].get() : "";
+ entry.sdpMLineIndex = body.contains("sdpMLineIndex") && body["sdpMLineIndex"].is_number_integer()
+ ? (int)body["sdpMLineIndex"].get() : 0;
+ entry.at = OSUtils::now();
+ if (entry.candidate.empty()) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"candidate is required\"}");
+ return;
+ }
+ Mutex::Lock _l(g_commsMutex);
+ g_iceByCall[callId].push_back(entry);
+ nlohmann::json out = nlohmann::json::object();
+ out["ok"] = true;
+ setContent(req, res, out.dump());
+ } catch ( ... ) {
+ res.status = 400;
+ setContent(req, res, "{\"error\":\"invalid JSON\"}");
+ }
+ };
+ s.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
+ sv6.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
+
+ auto callIceGet = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ int64_t since = 0;
+ if (req.has_param("since")) {
+ try {
+ since = std::stoll(req.get_param_value("since"));
+ } catch ( ... ) {
+ since = 0;
+ }
+ }
+ const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
+ nlohmann::json candidates = nlohmann::json::array();
+ Mutex::Lock _l(g_commsMutex);
+ std::map >::const_iterator it = g_iceByCall.find(callId);
+ if (it != g_iceByCall.end()) {
+ for (std::vector::const_iterator e = it->second.begin(); e != it->second.end(); ++e) {
+ if (e->at <= since) {
+ continue;
+ }
+ if (!peerId.empty() && e->fromPeerId == peerId) {
+ continue;
+ }
+ nlohmann::json item = nlohmann::json::object();
+ item["candidate"] = e->candidate;
+ item["sdpMid"] = e->sdpMid;
+ item["sdpMLineIndex"] = e->sdpMLineIndex;
+ item["at"] = e->at;
+ candidates.push_back(item);
+ }
+ }
+ nlohmann::json out = nlohmann::json::object();
+ out["candidates"] = candidates;
+ setContent(req, res, out.dump());
+ };
+ s.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
+ sv6.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
+
+ auto callGet = [setContent](const httplib::Request &req, httplib::Response &res) {
+ const std::string callId = req.matches[1];
+ Mutex::Lock _l(g_commsMutex);
+ std::map::const_iterator it = g_calls.find(callId);
+ if (it == g_calls.end()) {
+ res.status = 404;
+ setContent(req, res, "{\"error\":\"call not found\"}");
+ return;
+ }
+ nlohmann::json out = callToIncomingJson(it->second);
+ if (it->second.state == "accepted" && !it->second.answerSdp.empty()) {
+ out["answerSdp"] = it->second.answerSdp;
+ }
+ setContent(req, res, out.dump());
+ };
+ s.Get("/controller/comms/call/([^/]+)", callGet);
+ sv6.Get("/controller/comms/call/([^/]+)", callGet);
+}
+
+} // namespace ZeroTier
diff --git a/controller/CommsController.hpp b/controller/CommsController.hpp
new file mode 100644
index 0000000..7228464
--- /dev/null
+++ b/controller/CommsController.hpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c)2019 ZeroTier, Inc.
+ *
+ * Use of this software is governed by the Business Source License included
+ * in the LICENSE.TXT file in the project's root directory.
+ */
+/****/
+
+#ifndef ZT_COMMSCONTROLLER_HPP
+#define ZT_COMMSCONTROLLER_HPP
+
+#include
+#include
+
+#include
+#include
+
+namespace ZeroTier {
+
+/**
+ * Ephemeral in-memory WebRTC signaling fallback and webrtc-config API.
+ */
+class CommsController
+{
+public:
+ void configureRoutes(
+ httplib::Server &s,
+ httplib::Server &sv6,
+ const std::function &localConfigGetter,
+ const std::function &setContent);
+};
+
+} // namespace ZeroTier
+
+#endif
diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp
index 14c3725..7dee499 100644
--- a/controller/EmbeddedNetworkController.cpp
+++ b/controller/EmbeddedNetworkController.cpp
@@ -1120,6 +1120,12 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
};
s.Get(memberListPath2, memberListGet2);
sv6.Get(memberListPath2, memberListGet2);
+ std::string appMemberListPath = "/app/api/network/([0-9a-fA-F]{16})/members";
+ s.Get(appMemberListPath, memberListGet2);
+ sv6.Get(appMemberListPath, memberListGet2);
+ std::string backoneMemberListPath = "/api/backone/network/([0-9a-fA-F]{16})/members";
+ s.Get(backoneMemberListPath, memberListGet2);
+ sv6.Get(backoneMemberListPath, memberListGet2);
auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
auto networkID = req.matches[1];
diff --git a/debian/.debhelper/bucket/files/1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f b/debian/.debhelper/bucket/files/1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f
new file mode 100755
index 0000000..dd2ca93
--- /dev/null
+++ b/debian/.debhelper/bucket/files/1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f
@@ -0,0 +1,1825 @@
+#! /bin/sh
+# Configuration validation subroutine script.
+# Copyright 1992-2016 Free Software Foundation, Inc.
+
+timestamp='2016-11-04'
+
+# This file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see .
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that
+# program. This Exception is an additional permission under section 7
+# of the GNU General Public License, version 3 ("GPLv3").
+
+
+# Please send patches to .
+#
+# Configuration subroutine to validate and canonicalize a configuration type.
+# Supply the specified configuration type as an argument.
+# If it is invalid, we print an error message on stderr and exit with code 1.
+# Otherwise, we print the canonical config type on stdout and succeed.
+
+# You can get the latest version of this script from:
+# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub
+
+# This file is supposed to be the same for all GNU packages
+# and recognize all the CPU types, system types and aliases
+# that are meaningful with *any* GNU software.
+# Each package is responsible for reporting which valid configurations
+# it does not support. The user should be able to distinguish
+# a failure to support a valid configuration from a meaningless
+# configuration.
+
+# The goal of this file is to map all the various variations of a given
+# machine specification into a single specification in the form:
+# CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
+# or in some cases, the newer four-part form:
+# CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
+# It is wrong to echo any other type of specification.
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION] CPU-MFR-OPSYS or ALIAS
+
+Canonicalize a configuration name.
+
+Operation modes:
+ -h, --help print this help, then exit
+ -t, --time-stamp print date of last modification, then exit
+ -v, --version print version number, then exit
+
+Report bugs and patches to ."
+
+version="\
+GNU config.sub ($timestamp)
+
+Copyright 1992-2016 Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+ case $1 in
+ --time-stamp | --time* | -t )
+ echo "$timestamp" ; exit ;;
+ --version | -v )
+ echo "$version" ; exit ;;
+ --help | --h* | -h )
+ echo "$usage"; exit ;;
+ -- ) # Stop option processing
+ shift; break ;;
+ - ) # Use stdin as input.
+ break ;;
+ -* )
+ echo "$me: invalid option $1$help"
+ exit 1 ;;
+
+ *local*)
+ # First pass through any local machine types.
+ echo $1
+ exit ;;
+
+ * )
+ break ;;
+ esac
+done
+
+case $# in
+ 0) echo "$me: missing argument$help" >&2
+ exit 1;;
+ 1) ;;
+ *) echo "$me: too many arguments$help" >&2
+ exit 1;;
+esac
+
+# Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any).
+# Here we must recognize all the valid KERNEL-OS combinations.
+maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'`
+case $maybe_os in
+ nto-qnx* | linux-gnu* | linux-android* | linux-dietlibc | linux-newlib* | \
+ linux-musl* | linux-uclibc* | uclinux-uclibc* | uclinux-gnu* | kfreebsd*-gnu* | \
+ knetbsd*-gnu* | netbsd*-gnu* | netbsd*-eabi* | \
+ kopensolaris*-gnu* | cloudabi*-eabi* | \
+ storm-chaos* | os2-emx* | rtmk-nova*)
+ os=-$maybe_os
+ basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`
+ ;;
+ android-linux)
+ os=-linux-android
+ basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`-unknown
+ ;;
+ *)
+ basic_machine=`echo $1 | sed 's/-[^-]*$//'`
+ if [ $basic_machine != $1 ]
+ then os=`echo $1 | sed 's/.*-/-/'`
+ else os=; fi
+ ;;
+esac
+
+### Let's recognize common machines as not being operating systems so
+### that things like config.sub decstation-3100 work. We also
+### recognize some manufacturers as not being operating systems, so we
+### can provide default operating systems below.
+case $os in
+ -sun*os*)
+ # Prevent following clause from handling this invalid input.
+ ;;
+ -dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \
+ -att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \
+ -unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \
+ -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\
+ -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \
+ -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \
+ -apple | -axis | -knuth | -cray | -microblaze*)
+ os=
+ basic_machine=$1
+ ;;
+ -bluegene*)
+ os=-cnk
+ ;;
+ -sim | -cisco | -oki | -wec | -winbond)
+ os=
+ basic_machine=$1
+ ;;
+ -scout)
+ ;;
+ -wrs)
+ os=-vxworks
+ basic_machine=$1
+ ;;
+ -chorusos*)
+ os=-chorusos
+ basic_machine=$1
+ ;;
+ -chorusrdb)
+ os=-chorusrdb
+ basic_machine=$1
+ ;;
+ -hiux*)
+ os=-hiuxwe2
+ ;;
+ -sco6)
+ os=-sco5v6
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco5)
+ os=-sco3.2v5
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco4)
+ os=-sco3.2v4
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco3.2.[4-9]*)
+ os=`echo $os | sed -e 's/sco3.2./sco3.2v/'`
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco3.2v[4-9]*)
+ # Don't forget version if it is 3.2v4 or newer.
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco5v6*)
+ # Don't forget version if it is 3.2v4 or newer.
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -sco*)
+ os=-sco3.2v2
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -udk*)
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -isc)
+ os=-isc2.2
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -clix*)
+ basic_machine=clipper-intergraph
+ ;;
+ -isc*)
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+ ;;
+ -lynx*178)
+ os=-lynxos178
+ ;;
+ -lynx*5)
+ os=-lynxos5
+ ;;
+ -lynx*)
+ os=-lynxos
+ ;;
+ -ptx*)
+ basic_machine=`echo $1 | sed -e 's/86-.*/86-sequent/'`
+ ;;
+ -windowsnt*)
+ os=`echo $os | sed -e 's/windowsnt/winnt/'`
+ ;;
+ -psos*)
+ os=-psos
+ ;;
+ -mint | -mint[0-9]*)
+ basic_machine=m68k-atari
+ os=-mint
+ ;;
+esac
+
+# Decode aliases for certain CPU-COMPANY combinations.
+case $basic_machine in
+ # Recognize the basic CPU types without company name.
+ # Some are omitted here because they have special meanings below.
+ 1750a | 580 \
+ | a29k \
+ | aarch64 | aarch64_be \
+ | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \
+ | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \
+ | am33_2.0 \
+ | arc | arceb \
+ | arm | arm[bl]e | arme[lb] | armv[2-8] | armv[3-8][lb] | armv7[arm] \
+ | avr | avr32 \
+ | ba \
+ | be32 | be64 \
+ | bfin \
+ | c4x | c8051 | clipper \
+ | d10v | d30v | dlx | dsp16xx \
+ | e2k | epiphany \
+ | fido | fr30 | frv | ft32 \
+ | h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \
+ | hexagon \
+ | i370 | i860 | i960 | ia64 \
+ | ip2k | iq2000 \
+ | k1om \
+ | le32 | le64 \
+ | lm32 \
+ | m32c | m32r | m32rle | m68000 | m68k | m88k \
+ | maxq | mb | microblaze | microblazeel | mcore | mep | metag \
+ | mips | mipsbe | mipseb | mipsel | mipsle \
+ | mips16 \
+ | mips64 | mips64el \
+ | mips64octeon | mips64octeonel \
+ | mips64orion | mips64orionel \
+ | mips64r5900 | mips64r5900el \
+ | mips64vr | mips64vrel \
+ | mips64vr4100 | mips64vr4100el \
+ | mips64vr4300 | mips64vr4300el \
+ | mips64vr5000 | mips64vr5000el \
+ | mips64vr5900 | mips64vr5900el \
+ | mipsisa32 | mipsisa32el \
+ | mipsisa32r2 | mipsisa32r2el \
+ | mipsisa32r6 | mipsisa32r6el \
+ | mipsisa64 | mipsisa64el \
+ | mipsisa64r2 | mipsisa64r2el \
+ | mipsisa64r6 | mipsisa64r6el \
+ | mipsisa64sb1 | mipsisa64sb1el \
+ | mipsisa64sr71k | mipsisa64sr71kel \
+ | mipsr5900 | mipsr5900el \
+ | mipstx39 | mipstx39el \
+ | mn10200 | mn10300 \
+ | moxie \
+ | mt \
+ | msp430 \
+ | nds32 | nds32le | nds32be \
+ | nios | nios2 | nios2eb | nios2el \
+ | ns16k | ns32k \
+ | open8 | or1k | or1knd | or32 \
+ | pdp10 | pdp11 | pj | pjl \
+ | powerpc | powerpc64 | powerpc64le | powerpcle \
+ | pru \
+ | pyramid \
+ | riscv32 | riscv64 \
+ | rl78 | rx \
+ | score \
+ | sh | sh[1234] | sh[24]a | sh[24]aeb | sh[23]e | sh[234]eb | sheb | shbe | shle | sh[1234]le | sh3ele \
+ | sh64 | sh64le \
+ | sparc | sparc64 | sparc64b | sparc64v | sparc86x | sparclet | sparclite \
+ | sparcv8 | sparcv9 | sparcv9b | sparcv9v \
+ | spu \
+ | tahoe | tic4x | tic54x | tic55x | tic6x | tic80 | tron \
+ | ubicom32 \
+ | v850 | v850e | v850e1 | v850e2 | v850es | v850e2v3 \
+ | visium \
+ | we32k \
+ | x86 | xc16x | xstormy16 | xtensa \
+ | z8k | z80)
+ basic_machine=$basic_machine-unknown
+ ;;
+ c54x)
+ basic_machine=tic54x-unknown
+ ;;
+ c55x)
+ basic_machine=tic55x-unknown
+ ;;
+ c6x)
+ basic_machine=tic6x-unknown
+ ;;
+ leon|leon[3-9])
+ basic_machine=sparc-$basic_machine
+ ;;
+ m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x | nvptx | picochip)
+ basic_machine=$basic_machine-unknown
+ os=-none
+ ;;
+ m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | v70 | w65 | z8k)
+ ;;
+ ms1)
+ basic_machine=mt-unknown
+ ;;
+
+ strongarm | thumb | xscale)
+ basic_machine=arm-unknown
+ ;;
+ xgate)
+ basic_machine=$basic_machine-unknown
+ os=-none
+ ;;
+ xscaleeb)
+ basic_machine=armeb-unknown
+ ;;
+
+ xscaleel)
+ basic_machine=armel-unknown
+ ;;
+
+ # We use `pc' rather than `unknown'
+ # because (1) that's what they normally are, and
+ # (2) the word "unknown" tends to confuse beginning users.
+ i*86 | x86_64)
+ basic_machine=$basic_machine-pc
+ ;;
+ # Object if more than one company name word.
+ *-*-*)
+ echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+ exit 1
+ ;;
+ # Recognize the basic CPU types with company name.
+ 580-* \
+ | a29k-* \
+ | aarch64-* | aarch64_be-* \
+ | alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \
+ | alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \
+ | alphapca5[67]-* | alpha64pca5[67]-* | arc-* | arceb-* \
+ | arm-* | armbe-* | armle-* | armeb-* | armv*-* \
+ | avr-* | avr32-* \
+ | ba-* \
+ | be32-* | be64-* \
+ | bfin-* | bs2000-* \
+ | c[123]* | c30-* | [cjt]90-* | c4x-* \
+ | c8051-* | clipper-* | craynv-* | cydra-* \
+ | d10v-* | d30v-* | dlx-* \
+ | e2k-* | elxsi-* \
+ | f30[01]-* | f700-* | fido-* | fr30-* | frv-* | fx80-* \
+ | h8300-* | h8500-* \
+ | hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \
+ | hexagon-* \
+ | i*86-* | i860-* | i960-* | ia64-* \
+ | ip2k-* | iq2000-* \
+ | k1om-* \
+ | le32-* | le64-* \
+ | lm32-* \
+ | m32c-* | m32r-* | m32rle-* \
+ | m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \
+ | m88110-* | m88k-* | maxq-* | mcore-* | metag-* \
+ | microblaze-* | microblazeel-* \
+ | mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \
+ | mips16-* \
+ | mips64-* | mips64el-* \
+ | mips64octeon-* | mips64octeonel-* \
+ | mips64orion-* | mips64orionel-* \
+ | mips64r5900-* | mips64r5900el-* \
+ | mips64vr-* | mips64vrel-* \
+ | mips64vr4100-* | mips64vr4100el-* \
+ | mips64vr4300-* | mips64vr4300el-* \
+ | mips64vr5000-* | mips64vr5000el-* \
+ | mips64vr5900-* | mips64vr5900el-* \
+ | mipsisa32-* | mipsisa32el-* \
+ | mipsisa32r2-* | mipsisa32r2el-* \
+ | mipsisa32r6-* | mipsisa32r6el-* \
+ | mipsisa64-* | mipsisa64el-* \
+ | mipsisa64r2-* | mipsisa64r2el-* \
+ | mipsisa64r6-* | mipsisa64r6el-* \
+ | mipsisa64sb1-* | mipsisa64sb1el-* \
+ | mipsisa64sr71k-* | mipsisa64sr71kel-* \
+ | mipsr5900-* | mipsr5900el-* \
+ | mipstx39-* | mipstx39el-* \
+ | mmix-* \
+ | mt-* \
+ | msp430-* \
+ | nds32-* | nds32le-* | nds32be-* \
+ | nios-* | nios2-* | nios2eb-* | nios2el-* \
+ | none-* | np1-* | ns16k-* | ns32k-* \
+ | open8-* \
+ | or1k*-* \
+ | orion-* \
+ | pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \
+ | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* \
+ | pru-* \
+ | pyramid-* \
+ | riscv32-* | riscv64-* \
+ | rl78-* | romp-* | rs6000-* | rx-* \
+ | sh-* | sh[1234]-* | sh[24]a-* | sh[24]aeb-* | sh[23]e-* | sh[34]eb-* | sheb-* | shbe-* \
+ | shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \
+ | sparc-* | sparc64-* | sparc64b-* | sparc64v-* | sparc86x-* | sparclet-* \
+ | sparclite-* \
+ | sparcv8-* | sparcv9-* | sparcv9b-* | sparcv9v-* | sv1-* | sx*-* \
+ | tahoe-* \
+ | tic30-* | tic4x-* | tic54x-* | tic55x-* | tic6x-* | tic80-* \
+ | tile*-* \
+ | tron-* \
+ | ubicom32-* \
+ | v850-* | v850e-* | v850e1-* | v850es-* | v850e2-* | v850e2v3-* \
+ | vax-* \
+ | visium-* \
+ | we32k-* \
+ | x86-* | x86_64-* | xc16x-* | xps100-* \
+ | xstormy16-* | xtensa*-* \
+ | ymp-* \
+ | z8k-* | z80-*)
+ ;;
+ # Recognize the basic CPU types without company name, with glob match.
+ xtensa*)
+ basic_machine=$basic_machine-unknown
+ ;;
+ # Recognize the various machine names and aliases which stand
+ # for a CPU type and a company and sometimes even an OS.
+ 386bsd)
+ basic_machine=i386-unknown
+ os=-bsd
+ ;;
+ 3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc)
+ basic_machine=m68000-att
+ ;;
+ 3b*)
+ basic_machine=we32k-att
+ ;;
+ a29khif)
+ basic_machine=a29k-amd
+ os=-udi
+ ;;
+ abacus)
+ basic_machine=abacus-unknown
+ ;;
+ adobe68k)
+ basic_machine=m68010-adobe
+ os=-scout
+ ;;
+ alliant | fx80)
+ basic_machine=fx80-alliant
+ ;;
+ altos | altos3068)
+ basic_machine=m68k-altos
+ ;;
+ am29k)
+ basic_machine=a29k-none
+ os=-bsd
+ ;;
+ amd64)
+ basic_machine=x86_64-pc
+ ;;
+ amd64-*)
+ basic_machine=x86_64-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ amdahl)
+ basic_machine=580-amdahl
+ os=-sysv
+ ;;
+ amiga | amiga-*)
+ basic_machine=m68k-unknown
+ ;;
+ amigaos | amigados)
+ basic_machine=m68k-unknown
+ os=-amigaos
+ ;;
+ amigaunix | amix)
+ basic_machine=m68k-unknown
+ os=-sysv4
+ ;;
+ apollo68)
+ basic_machine=m68k-apollo
+ os=-sysv
+ ;;
+ apollo68bsd)
+ basic_machine=m68k-apollo
+ os=-bsd
+ ;;
+ aros)
+ basic_machine=i386-pc
+ os=-aros
+ ;;
+ asmjs)
+ basic_machine=asmjs-unknown
+ ;;
+ aux)
+ basic_machine=m68k-apple
+ os=-aux
+ ;;
+ balance)
+ basic_machine=ns32k-sequent
+ os=-dynix
+ ;;
+ blackfin)
+ basic_machine=bfin-unknown
+ os=-linux
+ ;;
+ blackfin-*)
+ basic_machine=bfin-`echo $basic_machine | sed 's/^[^-]*-//'`
+ os=-linux
+ ;;
+ bluegene*)
+ basic_machine=powerpc-ibm
+ os=-cnk
+ ;;
+ c54x-*)
+ basic_machine=tic54x-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ c55x-*)
+ basic_machine=tic55x-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ c6x-*)
+ basic_machine=tic6x-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ c90)
+ basic_machine=c90-cray
+ os=-unicos
+ ;;
+ cegcc)
+ basic_machine=arm-unknown
+ os=-cegcc
+ ;;
+ convex-c1)
+ basic_machine=c1-convex
+ os=-bsd
+ ;;
+ convex-c2)
+ basic_machine=c2-convex
+ os=-bsd
+ ;;
+ convex-c32)
+ basic_machine=c32-convex
+ os=-bsd
+ ;;
+ convex-c34)
+ basic_machine=c34-convex
+ os=-bsd
+ ;;
+ convex-c38)
+ basic_machine=c38-convex
+ os=-bsd
+ ;;
+ cray | j90)
+ basic_machine=j90-cray
+ os=-unicos
+ ;;
+ craynv)
+ basic_machine=craynv-cray
+ os=-unicosmp
+ ;;
+ cr16 | cr16-*)
+ basic_machine=cr16-unknown
+ os=-elf
+ ;;
+ crds | unos)
+ basic_machine=m68k-crds
+ ;;
+ crisv32 | crisv32-* | etraxfs*)
+ basic_machine=crisv32-axis
+ ;;
+ cris | cris-* | etrax*)
+ basic_machine=cris-axis
+ ;;
+ crx)
+ basic_machine=crx-unknown
+ os=-elf
+ ;;
+ da30 | da30-*)
+ basic_machine=m68k-da30
+ ;;
+ decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn)
+ basic_machine=mips-dec
+ ;;
+ decsystem10* | dec10*)
+ basic_machine=pdp10-dec
+ os=-tops10
+ ;;
+ decsystem20* | dec20*)
+ basic_machine=pdp10-dec
+ os=-tops20
+ ;;
+ delta | 3300 | motorola-3300 | motorola-delta \
+ | 3300-motorola | delta-motorola)
+ basic_machine=m68k-motorola
+ ;;
+ delta88)
+ basic_machine=m88k-motorola
+ os=-sysv3
+ ;;
+ dicos)
+ basic_machine=i686-pc
+ os=-dicos
+ ;;
+ djgpp)
+ basic_machine=i586-pc
+ os=-msdosdjgpp
+ ;;
+ dpx20 | dpx20-*)
+ basic_machine=rs6000-bull
+ os=-bosx
+ ;;
+ dpx2* | dpx2*-bull)
+ basic_machine=m68k-bull
+ os=-sysv3
+ ;;
+ e500v[12])
+ basic_machine=powerpc-unknown
+ os=$os"spe"
+ ;;
+ e500v[12]-*)
+ basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'`
+ os=$os"spe"
+ ;;
+ ebmon29k)
+ basic_machine=a29k-amd
+ os=-ebmon
+ ;;
+ elxsi)
+ basic_machine=elxsi-elxsi
+ os=-bsd
+ ;;
+ encore | umax | mmax)
+ basic_machine=ns32k-encore
+ ;;
+ es1800 | OSE68k | ose68k | ose | OSE)
+ basic_machine=m68k-ericsson
+ os=-ose
+ ;;
+ fx2800)
+ basic_machine=i860-alliant
+ ;;
+ genix)
+ basic_machine=ns32k-ns
+ ;;
+ gmicro)
+ basic_machine=tron-gmicro
+ os=-sysv
+ ;;
+ go32)
+ basic_machine=i386-pc
+ os=-go32
+ ;;
+ h3050r* | hiux*)
+ basic_machine=hppa1.1-hitachi
+ os=-hiuxwe2
+ ;;
+ h8300hms)
+ basic_machine=h8300-hitachi
+ os=-hms
+ ;;
+ h8300xray)
+ basic_machine=h8300-hitachi
+ os=-xray
+ ;;
+ h8500hms)
+ basic_machine=h8500-hitachi
+ os=-hms
+ ;;
+ harris)
+ basic_machine=m88k-harris
+ os=-sysv3
+ ;;
+ hp300-*)
+ basic_machine=m68k-hp
+ ;;
+ hp300bsd)
+ basic_machine=m68k-hp
+ os=-bsd
+ ;;
+ hp300hpux)
+ basic_machine=m68k-hp
+ os=-hpux
+ ;;
+ hp3k9[0-9][0-9] | hp9[0-9][0-9])
+ basic_machine=hppa1.0-hp
+ ;;
+ hp9k2[0-9][0-9] | hp9k31[0-9])
+ basic_machine=m68000-hp
+ ;;
+ hp9k3[2-9][0-9])
+ basic_machine=m68k-hp
+ ;;
+ hp9k6[0-9][0-9] | hp6[0-9][0-9])
+ basic_machine=hppa1.0-hp
+ ;;
+ hp9k7[0-79][0-9] | hp7[0-79][0-9])
+ basic_machine=hppa1.1-hp
+ ;;
+ hp9k78[0-9] | hp78[0-9])
+ # FIXME: really hppa2.0-hp
+ basic_machine=hppa1.1-hp
+ ;;
+ hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893)
+ # FIXME: really hppa2.0-hp
+ basic_machine=hppa1.1-hp
+ ;;
+ hp9k8[0-9][13679] | hp8[0-9][13679])
+ basic_machine=hppa1.1-hp
+ ;;
+ hp9k8[0-9][0-9] | hp8[0-9][0-9])
+ basic_machine=hppa1.0-hp
+ ;;
+ hppa-next)
+ os=-nextstep3
+ ;;
+ hppaosf)
+ basic_machine=hppa1.1-hp
+ os=-osf
+ ;;
+ hppro)
+ basic_machine=hppa1.1-hp
+ os=-proelf
+ ;;
+ i370-ibm* | ibm*)
+ basic_machine=i370-ibm
+ ;;
+ i*86v32)
+ basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+ os=-sysv32
+ ;;
+ i*86v4*)
+ basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+ os=-sysv4
+ ;;
+ i*86v)
+ basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+ os=-sysv
+ ;;
+ i*86sol2)
+ basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+ os=-solaris2
+ ;;
+ i386mach)
+ basic_machine=i386-mach
+ os=-mach
+ ;;
+ i386-vsta | vsta)
+ basic_machine=i386-unknown
+ os=-vsta
+ ;;
+ iris | iris4d)
+ basic_machine=mips-sgi
+ case $os in
+ -irix*)
+ ;;
+ *)
+ os=-irix4
+ ;;
+ esac
+ ;;
+ isi68 | isi)
+ basic_machine=m68k-isi
+ os=-sysv
+ ;;
+ leon-*|leon[3-9]-*)
+ basic_machine=sparc-`echo $basic_machine | sed 's/-.*//'`
+ ;;
+ m68knommu)
+ basic_machine=m68k-unknown
+ os=-linux
+ ;;
+ m68knommu-*)
+ basic_machine=m68k-`echo $basic_machine | sed 's/^[^-]*-//'`
+ os=-linux
+ ;;
+ m88k-omron*)
+ basic_machine=m88k-omron
+ ;;
+ magnum | m3230)
+ basic_machine=mips-mips
+ os=-sysv
+ ;;
+ merlin)
+ basic_machine=ns32k-utek
+ os=-sysv
+ ;;
+ microblaze*)
+ basic_machine=microblaze-xilinx
+ ;;
+ mingw64)
+ basic_machine=x86_64-pc
+ os=-mingw64
+ ;;
+ mingw32)
+ basic_machine=i686-pc
+ os=-mingw32
+ ;;
+ mingw32ce)
+ basic_machine=arm-unknown
+ os=-mingw32ce
+ ;;
+ miniframe)
+ basic_machine=m68000-convergent
+ ;;
+ *mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*)
+ basic_machine=m68k-atari
+ os=-mint
+ ;;
+ mips3*-*)
+ basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`
+ ;;
+ mips3*)
+ basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`-unknown
+ ;;
+ monitor)
+ basic_machine=m68k-rom68k
+ os=-coff
+ ;;
+ morphos)
+ basic_machine=powerpc-unknown
+ os=-morphos
+ ;;
+ moxiebox)
+ basic_machine=moxie-unknown
+ os=-moxiebox
+ ;;
+ msdos)
+ basic_machine=i386-pc
+ os=-msdos
+ ;;
+ ms1-*)
+ basic_machine=`echo $basic_machine | sed -e 's/ms1-/mt-/'`
+ ;;
+ msys)
+ basic_machine=i686-pc
+ os=-msys
+ ;;
+ mvs)
+ basic_machine=i370-ibm
+ os=-mvs
+ ;;
+ nacl)
+ basic_machine=le32-unknown
+ os=-nacl
+ ;;
+ ncr3000)
+ basic_machine=i486-ncr
+ os=-sysv4
+ ;;
+ netbsd386)
+ basic_machine=i386-unknown
+ os=-netbsd
+ ;;
+ netwinder)
+ basic_machine=armv4l-rebel
+ os=-linux
+ ;;
+ news | news700 | news800 | news900)
+ basic_machine=m68k-sony
+ os=-newsos
+ ;;
+ news1000)
+ basic_machine=m68030-sony
+ os=-newsos
+ ;;
+ news-3600 | risc-news)
+ basic_machine=mips-sony
+ os=-newsos
+ ;;
+ necv70)
+ basic_machine=v70-nec
+ os=-sysv
+ ;;
+ next | m*-next )
+ basic_machine=m68k-next
+ case $os in
+ -nextstep* )
+ ;;
+ -ns2*)
+ os=-nextstep2
+ ;;
+ *)
+ os=-nextstep3
+ ;;
+ esac
+ ;;
+ nh3000)
+ basic_machine=m68k-harris
+ os=-cxux
+ ;;
+ nh[45]000)
+ basic_machine=m88k-harris
+ os=-cxux
+ ;;
+ nindy960)
+ basic_machine=i960-intel
+ os=-nindy
+ ;;
+ mon960)
+ basic_machine=i960-intel
+ os=-mon960
+ ;;
+ nonstopux)
+ basic_machine=mips-compaq
+ os=-nonstopux
+ ;;
+ np1)
+ basic_machine=np1-gould
+ ;;
+ neo-tandem)
+ basic_machine=neo-tandem
+ ;;
+ nse-tandem)
+ basic_machine=nse-tandem
+ ;;
+ nsr-tandem)
+ basic_machine=nsr-tandem
+ ;;
+ op50n-* | op60c-*)
+ basic_machine=hppa1.1-oki
+ os=-proelf
+ ;;
+ openrisc | openrisc-*)
+ basic_machine=or32-unknown
+ ;;
+ os400)
+ basic_machine=powerpc-ibm
+ os=-os400
+ ;;
+ OSE68000 | ose68000)
+ basic_machine=m68000-ericsson
+ os=-ose
+ ;;
+ os68k)
+ basic_machine=m68k-none
+ os=-os68k
+ ;;
+ pa-hitachi)
+ basic_machine=hppa1.1-hitachi
+ os=-hiuxwe2
+ ;;
+ paragon)
+ basic_machine=i860-intel
+ os=-osf
+ ;;
+ parisc)
+ basic_machine=hppa-unknown
+ os=-linux
+ ;;
+ parisc-*)
+ basic_machine=hppa-`echo $basic_machine | sed 's/^[^-]*-//'`
+ os=-linux
+ ;;
+ pbd)
+ basic_machine=sparc-tti
+ ;;
+ pbb)
+ basic_machine=m68k-tti
+ ;;
+ pc532 | pc532-*)
+ basic_machine=ns32k-pc532
+ ;;
+ pc98)
+ basic_machine=i386-pc
+ ;;
+ pc98-*)
+ basic_machine=i386-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ pentium | p5 | k5 | k6 | nexgen | viac3)
+ basic_machine=i586-pc
+ ;;
+ pentiumpro | p6 | 6x86 | athlon | athlon_*)
+ basic_machine=i686-pc
+ ;;
+ pentiumii | pentium2 | pentiumiii | pentium3)
+ basic_machine=i686-pc
+ ;;
+ pentium4)
+ basic_machine=i786-pc
+ ;;
+ pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*)
+ basic_machine=i586-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ pentiumpro-* | p6-* | 6x86-* | athlon-*)
+ basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*)
+ basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ pentium4-*)
+ basic_machine=i786-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ pn)
+ basic_machine=pn-gould
+ ;;
+ power) basic_machine=power-ibm
+ ;;
+ ppc | ppcbe) basic_machine=powerpc-unknown
+ ;;
+ ppc-* | ppcbe-*)
+ basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ ppcle | powerpclittle)
+ basic_machine=powerpcle-unknown
+ ;;
+ ppcle-* | powerpclittle-*)
+ basic_machine=powerpcle-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ ppc64) basic_machine=powerpc64-unknown
+ ;;
+ ppc64-*) basic_machine=powerpc64-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ ppc64le | powerpc64little)
+ basic_machine=powerpc64le-unknown
+ ;;
+ ppc64le-* | powerpc64little-*)
+ basic_machine=powerpc64le-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ ps2)
+ basic_machine=i386-ibm
+ ;;
+ pw32)
+ basic_machine=i586-unknown
+ os=-pw32
+ ;;
+ rdos | rdos64)
+ basic_machine=x86_64-pc
+ os=-rdos
+ ;;
+ rdos32)
+ basic_machine=i386-pc
+ os=-rdos
+ ;;
+ rom68k)
+ basic_machine=m68k-rom68k
+ os=-coff
+ ;;
+ rm[46]00)
+ basic_machine=mips-siemens
+ ;;
+ rtpc | rtpc-*)
+ basic_machine=romp-ibm
+ ;;
+ s390 | s390-*)
+ basic_machine=s390-ibm
+ ;;
+ s390x | s390x-*)
+ basic_machine=s390x-ibm
+ ;;
+ sa29200)
+ basic_machine=a29k-amd
+ os=-udi
+ ;;
+ sb1)
+ basic_machine=mipsisa64sb1-unknown
+ ;;
+ sb1el)
+ basic_machine=mipsisa64sb1el-unknown
+ ;;
+ sde)
+ basic_machine=mipsisa32-sde
+ os=-elf
+ ;;
+ sei)
+ basic_machine=mips-sei
+ os=-seiux
+ ;;
+ sequent)
+ basic_machine=i386-sequent
+ ;;
+ sh)
+ basic_machine=sh-hitachi
+ os=-hms
+ ;;
+ sh5el)
+ basic_machine=sh5le-unknown
+ ;;
+ sh64)
+ basic_machine=sh64-unknown
+ ;;
+ sparclite-wrs | simso-wrs)
+ basic_machine=sparclite-wrs
+ os=-vxworks
+ ;;
+ sps7)
+ basic_machine=m68k-bull
+ os=-sysv2
+ ;;
+ spur)
+ basic_machine=spur-unknown
+ ;;
+ st2000)
+ basic_machine=m68k-tandem
+ ;;
+ stratus)
+ basic_machine=i860-stratus
+ os=-sysv4
+ ;;
+ strongarm-* | thumb-*)
+ basic_machine=arm-`echo $basic_machine | sed 's/^[^-]*-//'`
+ ;;
+ sun2)
+ basic_machine=m68000-sun
+ ;;
+ sun2os3)
+ basic_machine=m68000-sun
+ os=-sunos3
+ ;;
+ sun2os4)
+ basic_machine=m68000-sun
+ os=-sunos4
+ ;;
+ sun3os3)
+ basic_machine=m68k-sun
+ os=-sunos3
+ ;;
+ sun3os4)
+ basic_machine=m68k-sun
+ os=-sunos4
+ ;;
+ sun4os3)
+ basic_machine=sparc-sun
+ os=-sunos3
+ ;;
+ sun4os4)
+ basic_machine=sparc-sun
+ os=-sunos4
+ ;;
+ sun4sol2)
+ basic_machine=sparc-sun
+ os=-solaris2
+ ;;
+ sun3 | sun3-*)
+ basic_machine=m68k-sun
+ ;;
+ sun4)
+ basic_machine=sparc-sun
+ ;;
+ sun386 | sun386i | roadrunner)
+ basic_machine=i386-sun
+ ;;
+ sv1)
+ basic_machine=sv1-cray
+ os=-unicos
+ ;;
+ symmetry)
+ basic_machine=i386-sequent
+ os=-dynix
+ ;;
+ t3e)
+ basic_machine=alphaev5-cray
+ os=-unicos
+ ;;
+ t90)
+ basic_machine=t90-cray
+ os=-unicos
+ ;;
+ tile*)
+ basic_machine=$basic_machine-unknown
+ os=-linux-gnu
+ ;;
+ tx39)
+ basic_machine=mipstx39-unknown
+ ;;
+ tx39el)
+ basic_machine=mipstx39el-unknown
+ ;;
+ toad1)
+ basic_machine=pdp10-xkl
+ os=-tops20
+ ;;
+ tower | tower-32)
+ basic_machine=m68k-ncr
+ ;;
+ tpf)
+ basic_machine=s390x-ibm
+ os=-tpf
+ ;;
+ udi29k)
+ basic_machine=a29k-amd
+ os=-udi
+ ;;
+ ultra3)
+ basic_machine=a29k-nyu
+ os=-sym1
+ ;;
+ v810 | necv810)
+ basic_machine=v810-nec
+ os=-none
+ ;;
+ vaxv)
+ basic_machine=vax-dec
+ os=-sysv
+ ;;
+ vms)
+ basic_machine=vax-dec
+ os=-vms
+ ;;
+ vpp*|vx|vx-*)
+ basic_machine=f301-fujitsu
+ ;;
+ vxworks960)
+ basic_machine=i960-wrs
+ os=-vxworks
+ ;;
+ vxworks68)
+ basic_machine=m68k-wrs
+ os=-vxworks
+ ;;
+ vxworks29k)
+ basic_machine=a29k-wrs
+ os=-vxworks
+ ;;
+ w65*)
+ basic_machine=w65-wdc
+ os=-none
+ ;;
+ w89k-*)
+ basic_machine=hppa1.1-winbond
+ os=-proelf
+ ;;
+ xbox)
+ basic_machine=i686-pc
+ os=-mingw32
+ ;;
+ xps | xps100)
+ basic_machine=xps100-honeywell
+ ;;
+ xscale-* | xscalee[bl]-*)
+ basic_machine=`echo $basic_machine | sed 's/^xscale/arm/'`
+ ;;
+ ymp)
+ basic_machine=ymp-cray
+ os=-unicos
+ ;;
+ z8k-*-coff)
+ basic_machine=z8k-unknown
+ os=-sim
+ ;;
+ z80-*-coff)
+ basic_machine=z80-unknown
+ os=-sim
+ ;;
+ none)
+ basic_machine=none-none
+ os=-none
+ ;;
+
+# Here we handle the default manufacturer of certain CPU types. It is in
+# some cases the only manufacturer, in others, it is the most popular.
+ w89k)
+ basic_machine=hppa1.1-winbond
+ ;;
+ op50n)
+ basic_machine=hppa1.1-oki
+ ;;
+ op60c)
+ basic_machine=hppa1.1-oki
+ ;;
+ romp)
+ basic_machine=romp-ibm
+ ;;
+ mmix)
+ basic_machine=mmix-knuth
+ ;;
+ rs6000)
+ basic_machine=rs6000-ibm
+ ;;
+ vax)
+ basic_machine=vax-dec
+ ;;
+ pdp10)
+ # there are many clones, so DEC is not a safe bet
+ basic_machine=pdp10-unknown
+ ;;
+ pdp11)
+ basic_machine=pdp11-dec
+ ;;
+ we32k)
+ basic_machine=we32k-att
+ ;;
+ sh[1234] | sh[24]a | sh[24]aeb | sh[34]eb | sh[1234]le | sh[23]ele)
+ basic_machine=sh-unknown
+ ;;
+ sparc | sparcv8 | sparcv9 | sparcv9b | sparcv9v)
+ basic_machine=sparc-sun
+ ;;
+ cydra)
+ basic_machine=cydra-cydrome
+ ;;
+ orion)
+ basic_machine=orion-highlevel
+ ;;
+ orion105)
+ basic_machine=clipper-highlevel
+ ;;
+ mac | mpw | mac-mpw)
+ basic_machine=m68k-apple
+ ;;
+ pmac | pmac-mpw)
+ basic_machine=powerpc-apple
+ ;;
+ *-unknown)
+ # Make sure to match an already-canonicalized machine name.
+ ;;
+ *)
+ echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+ exit 1
+ ;;
+esac
+
+# Here we canonicalize certain aliases for manufacturers.
+case $basic_machine in
+ *-digital*)
+ basic_machine=`echo $basic_machine | sed 's/digital.*/dec/'`
+ ;;
+ *-commodore*)
+ basic_machine=`echo $basic_machine | sed 's/commodore.*/cbm/'`
+ ;;
+ *)
+ ;;
+esac
+
+# Decode manufacturer-specific aliases for certain operating systems.
+
+if [ x"$os" != x"" ]
+then
+case $os in
+ # First match some system type aliases
+ # that might get confused with valid system types.
+ # -solaris* is a basic system type, with this one exception.
+ -auroraux)
+ os=-auroraux
+ ;;
+ -solaris1 | -solaris1.*)
+ os=`echo $os | sed -e 's|solaris1|sunos4|'`
+ ;;
+ -solaris)
+ os=-solaris2
+ ;;
+ -svr4*)
+ os=-sysv4
+ ;;
+ -unixware*)
+ os=-sysv4.2uw
+ ;;
+ -gnu/linux*)
+ os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'`
+ ;;
+ # First accept the basic system types.
+ # The portable systems comes first.
+ # Each alternative MUST END IN A *, to match a version number.
+ # -sysv* is not here because it comes later, after sysvr4.
+ -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \
+ | -*vms* | -sco* | -esix* | -isc* | -aix* | -cnk* | -sunos | -sunos[34]*\
+ | -hpux* | -unos* | -osf* | -luna* | -dgux* | -auroraux* | -solaris* \
+ | -sym* | -kopensolaris* | -plan9* \
+ | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \
+ | -aos* | -aros* | -cloudabi* | -sortix* \
+ | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \
+ | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \
+ | -hiux* | -386bsd* | -knetbsd* | -mirbsd* | -netbsd* \
+ | -bitrig* | -openbsd* | -solidbsd* | -libertybsd* \
+ | -ekkobsd* | -kfreebsd* | -freebsd* | -riscix* | -lynxos* \
+ | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \
+ | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \
+ | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \
+ | -chorusos* | -chorusrdb* | -cegcc* \
+ | -cygwin* | -msys* | -pe* | -psos* | -moss* | -proelf* | -rtems* \
+ | -midipix* | -mingw32* | -mingw64* | -linux-gnu* | -linux-android* \
+ | -linux-newlib* | -linux-musl* | -linux-uclibc* \
+ | -uxpv* | -beos* | -mpeix* | -udk* | -moxiebox* \
+ | -interix* | -uwin* | -mks* | -rhapsody* | -darwin* | -opened* \
+ | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \
+ | -storm-chaos* | -tops10* | -tenex* | -tops20* | -its* \
+ | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \
+ | -morphos* | -superux* | -rtmk* | -rtmk-nova* | -windiss* \
+ | -powermax* | -dnix* | -nx6 | -nx7 | -sei* | -dragonfly* \
+ | -skyos* | -haiku* | -rdos* | -toppers* | -drops* | -es* \
+ | -onefs* | -tirtos* | -phoenix* | -fuchsia*)
+ # Remember, each alternative MUST END IN *, to match a version number.
+ ;;
+ -qnx*)
+ case $basic_machine in
+ x86-* | i*86-*)
+ ;;
+ *)
+ os=-nto$os
+ ;;
+ esac
+ ;;
+ -nto-qnx*)
+ ;;
+ -nto*)
+ os=`echo $os | sed -e 's|nto|nto-qnx|'`
+ ;;
+ -sim | -es1800* | -hms* | -xray | -os68k* | -none* | -v88r* \
+ | -windows* | -osx | -abug | -netware* | -os9* | -beos* | -haiku* \
+ | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*)
+ ;;
+ -mac*)
+ os=`echo $os | sed -e 's|mac|macos|'`
+ ;;
+ -linux-dietlibc)
+ os=-linux-dietlibc
+ ;;
+ -linux*)
+ os=`echo $os | sed -e 's|linux|linux-gnu|'`
+ ;;
+ -sunos5*)
+ os=`echo $os | sed -e 's|sunos5|solaris2|'`
+ ;;
+ -sunos6*)
+ os=`echo $os | sed -e 's|sunos6|solaris3|'`
+ ;;
+ -opened*)
+ os=-openedition
+ ;;
+ -os400*)
+ os=-os400
+ ;;
+ -wince*)
+ os=-wince
+ ;;
+ -osfrose*)
+ os=-osfrose
+ ;;
+ -osf*)
+ os=-osf
+ ;;
+ -utek*)
+ os=-bsd
+ ;;
+ -dynix*)
+ os=-bsd
+ ;;
+ -acis*)
+ os=-aos
+ ;;
+ -atheos*)
+ os=-atheos
+ ;;
+ -syllable*)
+ os=-syllable
+ ;;
+ -386bsd)
+ os=-bsd
+ ;;
+ -ctix* | -uts*)
+ os=-sysv
+ ;;
+ -nova*)
+ os=-rtmk-nova
+ ;;
+ -ns2 )
+ os=-nextstep2
+ ;;
+ -nsk*)
+ os=-nsk
+ ;;
+ # Preserve the version number of sinix5.
+ -sinix5.*)
+ os=`echo $os | sed -e 's|sinix|sysv|'`
+ ;;
+ -sinix*)
+ os=-sysv4
+ ;;
+ -tpf*)
+ os=-tpf
+ ;;
+ -triton*)
+ os=-sysv3
+ ;;
+ -oss*)
+ os=-sysv3
+ ;;
+ -svr4)
+ os=-sysv4
+ ;;
+ -svr3)
+ os=-sysv3
+ ;;
+ -sysvr4)
+ os=-sysv4
+ ;;
+ # This must come after -sysvr4.
+ -sysv*)
+ ;;
+ -ose*)
+ os=-ose
+ ;;
+ -es1800*)
+ os=-ose
+ ;;
+ -xenix)
+ os=-xenix
+ ;;
+ -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+ os=-mint
+ ;;
+ -aros*)
+ os=-aros
+ ;;
+ -zvmoe)
+ os=-zvmoe
+ ;;
+ -dicos*)
+ os=-dicos
+ ;;
+ -nacl*)
+ ;;
+ -ios)
+ ;;
+ -none)
+ ;;
+ *)
+ # Get rid of the `-' at the beginning of $os.
+ os=`echo $os | sed 's/[^-]*-//'`
+ echo Invalid configuration \`$1\': system \`$os\' not recognized 1>&2
+ exit 1
+ ;;
+esac
+else
+
+# Here we handle the default operating systems that come with various machines.
+# The value should be what the vendor currently ships out the door with their
+# machine or put another way, the most popular os provided with the machine.
+
+# Note that if you're going to try to match "-MANUFACTURER" here (say,
+# "-sun"), then you have to tell the case statement up towards the top
+# that MANUFACTURER isn't an operating system. Otherwise, code above
+# will signal an error saying that MANUFACTURER isn't an operating
+# system, and we'll never get to this point.
+
+case $basic_machine in
+ score-*)
+ os=-elf
+ ;;
+ spu-*)
+ os=-elf
+ ;;
+ *-acorn)
+ os=-riscix1.2
+ ;;
+ arm*-rebel)
+ os=-linux
+ ;;
+ arm*-semi)
+ os=-aout
+ ;;
+ c4x-* | tic4x-*)
+ os=-coff
+ ;;
+ c8051-*)
+ os=-elf
+ ;;
+ hexagon-*)
+ os=-elf
+ ;;
+ tic54x-*)
+ os=-coff
+ ;;
+ tic55x-*)
+ os=-coff
+ ;;
+ tic6x-*)
+ os=-coff
+ ;;
+ # This must come before the *-dec entry.
+ pdp10-*)
+ os=-tops20
+ ;;
+ pdp11-*)
+ os=-none
+ ;;
+ *-dec | vax-*)
+ os=-ultrix4.2
+ ;;
+ m68*-apollo)
+ os=-domain
+ ;;
+ i386-sun)
+ os=-sunos4.0.2
+ ;;
+ m68000-sun)
+ os=-sunos3
+ ;;
+ m68*-cisco)
+ os=-aout
+ ;;
+ mep-*)
+ os=-elf
+ ;;
+ mips*-cisco)
+ os=-elf
+ ;;
+ mips*-*)
+ os=-elf
+ ;;
+ or32-*)
+ os=-coff
+ ;;
+ *-tti) # must be before sparc entry or we get the wrong os.
+ os=-sysv3
+ ;;
+ sparc-* | *-sun)
+ os=-sunos4.1.1
+ ;;
+ *-be)
+ os=-beos
+ ;;
+ *-haiku)
+ os=-haiku
+ ;;
+ *-ibm)
+ os=-aix
+ ;;
+ *-knuth)
+ os=-mmixware
+ ;;
+ *-wec)
+ os=-proelf
+ ;;
+ *-winbond)
+ os=-proelf
+ ;;
+ *-oki)
+ os=-proelf
+ ;;
+ *-hp)
+ os=-hpux
+ ;;
+ *-hitachi)
+ os=-hiux
+ ;;
+ i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent)
+ os=-sysv
+ ;;
+ *-cbm)
+ os=-amigaos
+ ;;
+ *-dg)
+ os=-dgux
+ ;;
+ *-dolphin)
+ os=-sysv3
+ ;;
+ m68k-ccur)
+ os=-rtu
+ ;;
+ m88k-omron*)
+ os=-luna
+ ;;
+ *-next )
+ os=-nextstep
+ ;;
+ *-sequent)
+ os=-ptx
+ ;;
+ *-crds)
+ os=-unos
+ ;;
+ *-ns)
+ os=-genix
+ ;;
+ i370-*)
+ os=-mvs
+ ;;
+ *-next)
+ os=-nextstep3
+ ;;
+ *-gould)
+ os=-sysv
+ ;;
+ *-highlevel)
+ os=-bsd
+ ;;
+ *-encore)
+ os=-bsd
+ ;;
+ *-sgi)
+ os=-irix
+ ;;
+ *-siemens)
+ os=-sysv4
+ ;;
+ *-masscomp)
+ os=-rtu
+ ;;
+ f30[01]-fujitsu | f700-fujitsu)
+ os=-uxpv
+ ;;
+ *-rom68k)
+ os=-coff
+ ;;
+ *-*bug)
+ os=-coff
+ ;;
+ *-apple)
+ os=-macos
+ ;;
+ *-atari*)
+ os=-mint
+ ;;
+ *)
+ os=-none
+ ;;
+esac
+fi
+
+# Here we handle the case where we know the os, and the CPU type, but not the
+# manufacturer. We pick the logical manufacturer.
+vendor=unknown
+case $basic_machine in
+ *-unknown)
+ case $os in
+ -riscix*)
+ vendor=acorn
+ ;;
+ -sunos*)
+ vendor=sun
+ ;;
+ -cnk*|-aix*)
+ vendor=ibm
+ ;;
+ -beos*)
+ vendor=be
+ ;;
+ -hpux*)
+ vendor=hp
+ ;;
+ -mpeix*)
+ vendor=hp
+ ;;
+ -hiux*)
+ vendor=hitachi
+ ;;
+ -unos*)
+ vendor=crds
+ ;;
+ -dgux*)
+ vendor=dg
+ ;;
+ -luna*)
+ vendor=omron
+ ;;
+ -genix*)
+ vendor=ns
+ ;;
+ -mvs* | -opened*)
+ vendor=ibm
+ ;;
+ -os400*)
+ vendor=ibm
+ ;;
+ -ptx*)
+ vendor=sequent
+ ;;
+ -tpf*)
+ vendor=ibm
+ ;;
+ -vxsim* | -vxworks* | -windiss*)
+ vendor=wrs
+ ;;
+ -aux*)
+ vendor=apple
+ ;;
+ -hms*)
+ vendor=hitachi
+ ;;
+ -mpw* | -macos*)
+ vendor=apple
+ ;;
+ -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+ vendor=atari
+ ;;
+ -vos*)
+ vendor=stratus
+ ;;
+ esac
+ basic_machine=`echo $basic_machine | sed "s/unknown/$vendor/"`
+ ;;
+esac
+
+echo $basic_machine$os
+exit
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/debian/.debhelper/bucket/files/8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27 b/debian/.debhelper/bucket/files/8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27
new file mode 100755
index 0000000..2e9ad7f
--- /dev/null
+++ b/debian/.debhelper/bucket/files/8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27
@@ -0,0 +1,1462 @@
+#! /bin/sh
+# Attempt to guess a canonical system name.
+# Copyright 1992-2016 Free Software Foundation, Inc.
+
+timestamp='2016-10-02'
+
+# This file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see .
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that
+# program. This Exception is an additional permission under section 7
+# of the GNU General Public License, version 3 ("GPLv3").
+#
+# Originally written by Per Bothner; maintained since 2000 by Ben Elliston.
+#
+# You can get the latest version of this script from:
+# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess
+#
+# Please send patches to .
+
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION]
+
+Output the configuration name of the system \`$me' is run on.
+
+Operation modes:
+ -h, --help print this help, then exit
+ -t, --time-stamp print date of last modification, then exit
+ -v, --version print version number, then exit
+
+Report bugs and patches to ."
+
+version="\
+GNU config.guess ($timestamp)
+
+Originally written by Per Bothner.
+Copyright 1992-2016 Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+ case $1 in
+ --time-stamp | --time* | -t )
+ echo "$timestamp" ; exit ;;
+ --version | -v )
+ echo "$version" ; exit ;;
+ --help | --h* | -h )
+ echo "$usage"; exit ;;
+ -- ) # Stop option processing
+ shift; break ;;
+ - ) # Use stdin as input.
+ break ;;
+ -* )
+ echo "$me: invalid option $1$help" >&2
+ exit 1 ;;
+ * )
+ break ;;
+ esac
+done
+
+if test $# != 0; then
+ echo "$me: too many arguments$help" >&2
+ exit 1
+fi
+
+trap 'exit 1' 1 2 15
+
+# CC_FOR_BUILD -- compiler used by this script. Note that the use of a
+# compiler to aid in system detection is discouraged as it requires
+# temporary files to be created and, as you can see below, it is a
+# headache to deal with in a portable fashion.
+
+# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still
+# use `HOST_CC' if defined, but it is deprecated.
+
+# Portable tmp directory creation inspired by the Autoconf team.
+
+set_cc_for_build='
+trap "exitcode=\$?; (rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null) && exit \$exitcode" 0 ;
+trap "rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null; exit 1" 1 2 13 15 ;
+: ${TMPDIR=/tmp} ;
+ { tmp=`(umask 077 && mktemp -d "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } ||
+ { test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir $tmp) ; } ||
+ { tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir $tmp) && echo "Warning: creating insecure temp directory" >&2 ; } ||
+ { echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; } ;
+dummy=$tmp/dummy ;
+tmpfiles="$dummy.c $dummy.o $dummy.rel $dummy" ;
+case $CC_FOR_BUILD,$HOST_CC,$CC in
+ ,,) echo "int x;" > $dummy.c ;
+ for c in cc gcc c89 c99 ; do
+ if ($c -c -o $dummy.o $dummy.c) >/dev/null 2>&1 ; then
+ CC_FOR_BUILD="$c"; break ;
+ fi ;
+ done ;
+ if test x"$CC_FOR_BUILD" = x ; then
+ CC_FOR_BUILD=no_compiler_found ;
+ fi
+ ;;
+ ,,*) CC_FOR_BUILD=$CC ;;
+ ,*,*) CC_FOR_BUILD=$HOST_CC ;;
+esac ; set_cc_for_build= ;'
+
+# This is needed to find uname on a Pyramid OSx when run in the BSD universe.
+# (ghazi@noc.rutgers.edu 1994-08-24)
+if (test -f /.attbin/uname) >/dev/null 2>&1 ; then
+ PATH=$PATH:/.attbin ; export PATH
+fi
+
+UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown
+UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown
+UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown
+UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown
+
+case "${UNAME_SYSTEM}" in
+Linux|GNU|GNU/*)
+ # If the system lacks a compiler, then just pick glibc.
+ # We could probably try harder.
+ LIBC=gnu
+
+ eval $set_cc_for_build
+ cat <<-EOF > $dummy.c
+ #include
+ #if defined(__UCLIBC__)
+ LIBC=uclibc
+ #elif defined(__dietlibc__)
+ LIBC=dietlibc
+ #else
+ LIBC=gnu
+ #endif
+ EOF
+ eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^LIBC' | sed 's, ,,g'`
+ ;;
+esac
+
+# Note: order is significant - the case branches are not exclusive.
+
+case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in
+ *:NetBSD:*:*)
+ # NetBSD (nbsd) targets should (where applicable) match one or
+ # more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*,
+ # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently
+ # switched to ELF, *-*-netbsd* would select the old
+ # object file format. This provides both forward
+ # compatibility and a consistent mechanism for selecting the
+ # object file format.
+ #
+ # Note: NetBSD doesn't particularly care about the vendor
+ # portion of the name. We always set it to "unknown".
+ sysctl="sysctl -n hw.machine_arch"
+ UNAME_MACHINE_ARCH=`(uname -p 2>/dev/null || \
+ /sbin/$sysctl 2>/dev/null || \
+ /usr/sbin/$sysctl 2>/dev/null || \
+ echo unknown)`
+ case "${UNAME_MACHINE_ARCH}" in
+ armeb) machine=armeb-unknown ;;
+ arm*) machine=arm-unknown ;;
+ sh3el) machine=shl-unknown ;;
+ sh3eb) machine=sh-unknown ;;
+ sh5el) machine=sh5le-unknown ;;
+ earmv*)
+ arch=`echo ${UNAME_MACHINE_ARCH} | sed -e 's,^e\(armv[0-9]\).*$,\1,'`
+ endian=`echo ${UNAME_MACHINE_ARCH} | sed -ne 's,^.*\(eb\)$,\1,p'`
+ machine=${arch}${endian}-unknown
+ ;;
+ *) machine=${UNAME_MACHINE_ARCH}-unknown ;;
+ esac
+ # The Operating System including object format, if it has switched
+ # to ELF recently (or will in the future) and ABI.
+ case "${UNAME_MACHINE_ARCH}" in
+ earm*)
+ os=netbsdelf
+ ;;
+ arm*|i386|m68k|ns32k|sh3*|sparc|vax)
+ eval $set_cc_for_build
+ if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \
+ | grep -q __ELF__
+ then
+ # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout).
+ # Return netbsd for either. FIX?
+ os=netbsd
+ else
+ os=netbsdelf
+ fi
+ ;;
+ *)
+ os=netbsd
+ ;;
+ esac
+ # Determine ABI tags.
+ case "${UNAME_MACHINE_ARCH}" in
+ earm*)
+ expr='s/^earmv[0-9]/-eabi/;s/eb$//'
+ abi=`echo ${UNAME_MACHINE_ARCH} | sed -e "$expr"`
+ ;;
+ esac
+ # The OS release
+ # Debian GNU/NetBSD machines have a different userland, and
+ # thus, need a distinct triplet. However, they do not need
+ # kernel version information, so it can be replaced with a
+ # suitable tag, in the style of linux-gnu.
+ case "${UNAME_VERSION}" in
+ Debian*)
+ release='-gnu'
+ ;;
+ *)
+ release=`echo ${UNAME_RELEASE} | sed -e 's/[-_].*//' | cut -d. -f1,2`
+ ;;
+ esac
+ # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM:
+ # contains redundant information, the shorter form:
+ # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used.
+ echo "${machine}-${os}${release}${abi}"
+ exit ;;
+ *:Bitrig:*:*)
+ UNAME_MACHINE_ARCH=`arch | sed 's/Bitrig.//'`
+ echo ${UNAME_MACHINE_ARCH}-unknown-bitrig${UNAME_RELEASE}
+ exit ;;
+ *:OpenBSD:*:*)
+ UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'`
+ echo ${UNAME_MACHINE_ARCH}-unknown-openbsd${UNAME_RELEASE}
+ exit ;;
+ *:LibertyBSD:*:*)
+ UNAME_MACHINE_ARCH=`arch | sed 's/^.*BSD\.//'`
+ echo ${UNAME_MACHINE_ARCH}-unknown-libertybsd${UNAME_RELEASE}
+ exit ;;
+ *:ekkoBSD:*:*)
+ echo ${UNAME_MACHINE}-unknown-ekkobsd${UNAME_RELEASE}
+ exit ;;
+ *:SolidBSD:*:*)
+ echo ${UNAME_MACHINE}-unknown-solidbsd${UNAME_RELEASE}
+ exit ;;
+ macppc:MirBSD:*:*)
+ echo powerpc-unknown-mirbsd${UNAME_RELEASE}
+ exit ;;
+ *:MirBSD:*:*)
+ echo ${UNAME_MACHINE}-unknown-mirbsd${UNAME_RELEASE}
+ exit ;;
+ *:Sortix:*:*)
+ echo ${UNAME_MACHINE}-unknown-sortix
+ exit ;;
+ alpha:OSF1:*:*)
+ case $UNAME_RELEASE in
+ *4.0)
+ UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'`
+ ;;
+ *5.*)
+ UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'`
+ ;;
+ esac
+ # According to Compaq, /usr/sbin/psrinfo has been available on
+ # OSF/1 and Tru64 systems produced since 1995. I hope that
+ # covers most systems running today. This code pipes the CPU
+ # types through head -n 1, so we only detect the type of CPU 0.
+ ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^ The alpha \(.*\) processor.*$/\1/p' | head -n 1`
+ case "$ALPHA_CPU_TYPE" in
+ "EV4 (21064)")
+ UNAME_MACHINE=alpha ;;
+ "EV4.5 (21064)")
+ UNAME_MACHINE=alpha ;;
+ "LCA4 (21066/21068)")
+ UNAME_MACHINE=alpha ;;
+ "EV5 (21164)")
+ UNAME_MACHINE=alphaev5 ;;
+ "EV5.6 (21164A)")
+ UNAME_MACHINE=alphaev56 ;;
+ "EV5.6 (21164PC)")
+ UNAME_MACHINE=alphapca56 ;;
+ "EV5.7 (21164PC)")
+ UNAME_MACHINE=alphapca57 ;;
+ "EV6 (21264)")
+ UNAME_MACHINE=alphaev6 ;;
+ "EV6.7 (21264A)")
+ UNAME_MACHINE=alphaev67 ;;
+ "EV6.8CB (21264C)")
+ UNAME_MACHINE=alphaev68 ;;
+ "EV6.8AL (21264B)")
+ UNAME_MACHINE=alphaev68 ;;
+ "EV6.8CX (21264D)")
+ UNAME_MACHINE=alphaev68 ;;
+ "EV6.9A (21264/EV69A)")
+ UNAME_MACHINE=alphaev69 ;;
+ "EV7 (21364)")
+ UNAME_MACHINE=alphaev7 ;;
+ "EV7.9 (21364A)")
+ UNAME_MACHINE=alphaev79 ;;
+ esac
+ # A Pn.n version is a patched version.
+ # A Vn.n version is a released version.
+ # A Tn.n version is a released field test version.
+ # A Xn.n version is an unreleased experimental baselevel.
+ # 1.2 uses "1.2" for uname -r.
+ echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[PVTX]//' | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz`
+ # Reset EXIT trap before exiting to avoid spurious non-zero exit code.
+ exitcode=$?
+ trap '' 0
+ exit $exitcode ;;
+ Alpha\ *:Windows_NT*:*)
+ # How do we know it's Interix rather than the generic POSIX subsystem?
+ # Should we change UNAME_MACHINE based on the output of uname instead
+ # of the specific Alpha model?
+ echo alpha-pc-interix
+ exit ;;
+ 21064:Windows_NT:50:3)
+ echo alpha-dec-winnt3.5
+ exit ;;
+ Amiga*:UNIX_System_V:4.0:*)
+ echo m68k-unknown-sysv4
+ exit ;;
+ *:[Aa]miga[Oo][Ss]:*:*)
+ echo ${UNAME_MACHINE}-unknown-amigaos
+ exit ;;
+ *:[Mm]orph[Oo][Ss]:*:*)
+ echo ${UNAME_MACHINE}-unknown-morphos
+ exit ;;
+ *:OS/390:*:*)
+ echo i370-ibm-openedition
+ exit ;;
+ *:z/VM:*:*)
+ echo s390-ibm-zvmoe
+ exit ;;
+ *:OS400:*:*)
+ echo powerpc-ibm-os400
+ exit ;;
+ arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*)
+ echo arm-acorn-riscix${UNAME_RELEASE}
+ exit ;;
+ arm*:riscos:*:*|arm*:RISCOS:*:*)
+ echo arm-unknown-riscos
+ exit ;;
+ SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*)
+ echo hppa1.1-hitachi-hiuxmpp
+ exit ;;
+ Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*)
+ # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE.
+ if test "`(/bin/universe) 2>/dev/null`" = att ; then
+ echo pyramid-pyramid-sysv3
+ else
+ echo pyramid-pyramid-bsd
+ fi
+ exit ;;
+ NILE*:*:*:dcosx)
+ echo pyramid-pyramid-svr4
+ exit ;;
+ DRS?6000:unix:4.0:6*)
+ echo sparc-icl-nx6
+ exit ;;
+ DRS?6000:UNIX_SV:4.2*:7* | DRS?6000:isis:4.2*:7*)
+ case `/usr/bin/uname -p` in
+ sparc) echo sparc-icl-nx7; exit ;;
+ esac ;;
+ s390x:SunOS:*:*)
+ echo ${UNAME_MACHINE}-ibm-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ sun4H:SunOS:5.*:*)
+ echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*)
+ echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*)
+ echo i386-pc-auroraux${UNAME_RELEASE}
+ exit ;;
+ i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*)
+ eval $set_cc_for_build
+ SUN_ARCH=i386
+ # If there is a compiler, see if it is configured for 64-bit objects.
+ # Note that the Sun cc does not turn __LP64__ into 1 like gcc does.
+ # This test works for both compilers.
+ if [ "$CC_FOR_BUILD" != no_compiler_found ]; then
+ if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \
+ (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \
+ grep IS_64BIT_ARCH >/dev/null
+ then
+ SUN_ARCH=x86_64
+ fi
+ fi
+ echo ${SUN_ARCH}-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ sun4*:SunOS:6*:*)
+ # According to config.sub, this is the proper way to canonicalize
+ # SunOS6. Hard to guess exactly what SunOS6 will be like, but
+ # it's likely to be more like Solaris than SunOS4.
+ echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ sun4*:SunOS:*:*)
+ case "`/usr/bin/arch -k`" in
+ Series*|S4*)
+ UNAME_RELEASE=`uname -v`
+ ;;
+ esac
+ # Japanese Language versions have a version number like `4.1.3-JL'.
+ echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'`
+ exit ;;
+ sun3*:SunOS:*:*)
+ echo m68k-sun-sunos${UNAME_RELEASE}
+ exit ;;
+ sun*:*:4.2BSD:*)
+ UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null`
+ test "x${UNAME_RELEASE}" = x && UNAME_RELEASE=3
+ case "`/bin/arch`" in
+ sun3)
+ echo m68k-sun-sunos${UNAME_RELEASE}
+ ;;
+ sun4)
+ echo sparc-sun-sunos${UNAME_RELEASE}
+ ;;
+ esac
+ exit ;;
+ aushp:SunOS:*:*)
+ echo sparc-auspex-sunos${UNAME_RELEASE}
+ exit ;;
+ # The situation for MiNT is a little confusing. The machine name
+ # can be virtually everything (everything which is not
+ # "atarist" or "atariste" at least should have a processor
+ # > m68000). The system name ranges from "MiNT" over "FreeMiNT"
+ # to the lowercase version "mint" (or "freemint"). Finally
+ # the system name "TOS" denotes a system which is actually not
+ # MiNT. But MiNT is downward compatible to TOS, so this should
+ # be no problem.
+ atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*)
+ echo m68k-atari-mint${UNAME_RELEASE}
+ exit ;;
+ atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*)
+ echo m68k-atari-mint${UNAME_RELEASE}
+ exit ;;
+ *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*)
+ echo m68k-atari-mint${UNAME_RELEASE}
+ exit ;;
+ milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*)
+ echo m68k-milan-mint${UNAME_RELEASE}
+ exit ;;
+ hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*)
+ echo m68k-hades-mint${UNAME_RELEASE}
+ exit ;;
+ *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*)
+ echo m68k-unknown-mint${UNAME_RELEASE}
+ exit ;;
+ m68k:machten:*:*)
+ echo m68k-apple-machten${UNAME_RELEASE}
+ exit ;;
+ powerpc:machten:*:*)
+ echo powerpc-apple-machten${UNAME_RELEASE}
+ exit ;;
+ RISC*:Mach:*:*)
+ echo mips-dec-mach_bsd4.3
+ exit ;;
+ RISC*:ULTRIX:*:*)
+ echo mips-dec-ultrix${UNAME_RELEASE}
+ exit ;;
+ VAX*:ULTRIX*:*:*)
+ echo vax-dec-ultrix${UNAME_RELEASE}
+ exit ;;
+ 2020:CLIX:*:* | 2430:CLIX:*:*)
+ echo clipper-intergraph-clix${UNAME_RELEASE}
+ exit ;;
+ mips:*:*:UMIPS | mips:*:*:RISCos)
+ eval $set_cc_for_build
+ sed 's/^ //' << EOF >$dummy.c
+#ifdef __cplusplus
+#include /* for printf() prototype */
+ int main (int argc, char *argv[]) {
+#else
+ int main (argc, argv) int argc; char *argv[]; {
+#endif
+ #if defined (host_mips) && defined (MIPSEB)
+ #if defined (SYSTYPE_SYSV)
+ printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0);
+ #endif
+ #if defined (SYSTYPE_SVR4)
+ printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0);
+ #endif
+ #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD)
+ printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0);
+ #endif
+ #endif
+ exit (-1);
+ }
+EOF
+ $CC_FOR_BUILD -o $dummy $dummy.c &&
+ dummyarg=`echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` &&
+ SYSTEM_NAME=`$dummy $dummyarg` &&
+ { echo "$SYSTEM_NAME"; exit; }
+ echo mips-mips-riscos${UNAME_RELEASE}
+ exit ;;
+ Motorola:PowerMAX_OS:*:*)
+ echo powerpc-motorola-powermax
+ exit ;;
+ Motorola:*:4.3:PL8-*)
+ echo powerpc-harris-powermax
+ exit ;;
+ Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*)
+ echo powerpc-harris-powermax
+ exit ;;
+ Night_Hawk:Power_UNIX:*:*)
+ echo powerpc-harris-powerunix
+ exit ;;
+ m88k:CX/UX:7*:*)
+ echo m88k-harris-cxux7
+ exit ;;
+ m88k:*:4*:R4*)
+ echo m88k-motorola-sysv4
+ exit ;;
+ m88k:*:3*:R3*)
+ echo m88k-motorola-sysv3
+ exit ;;
+ AViiON:dgux:*:*)
+ # DG/UX returns AViiON for all architectures
+ UNAME_PROCESSOR=`/usr/bin/uname -p`
+ if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ]
+ then
+ if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \
+ [ ${TARGET_BINARY_INTERFACE}x = x ]
+ then
+ echo m88k-dg-dgux${UNAME_RELEASE}
+ else
+ echo m88k-dg-dguxbcs${UNAME_RELEASE}
+ fi
+ else
+ echo i586-dg-dgux${UNAME_RELEASE}
+ fi
+ exit ;;
+ M88*:DolphinOS:*:*) # DolphinOS (SVR3)
+ echo m88k-dolphin-sysv3
+ exit ;;
+ M88*:*:R3*:*)
+ # Delta 88k system running SVR3
+ echo m88k-motorola-sysv3
+ exit ;;
+ XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3)
+ echo m88k-tektronix-sysv3
+ exit ;;
+ Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD)
+ echo m68k-tektronix-bsd
+ exit ;;
+ *:IRIX*:*:*)
+ echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'`
+ exit ;;
+ ????????:AIX?:[12].1:2) # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX.
+ echo romp-ibm-aix # uname -m gives an 8 hex-code CPU id
+ exit ;; # Note that: echo "'`uname -s`'" gives 'AIX '
+ i*86:AIX:*:*)
+ echo i386-ibm-aix
+ exit ;;
+ ia64:AIX:*:*)
+ if [ -x /usr/bin/oslevel ] ; then
+ IBM_REV=`/usr/bin/oslevel`
+ else
+ IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE}
+ fi
+ echo ${UNAME_MACHINE}-ibm-aix${IBM_REV}
+ exit ;;
+ *:AIX:2:3)
+ if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then
+ eval $set_cc_for_build
+ sed 's/^ //' << EOF >$dummy.c
+ #include
+
+ main()
+ {
+ if (!__power_pc())
+ exit(1);
+ puts("powerpc-ibm-aix3.2.5");
+ exit(0);
+ }
+EOF
+ if $CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy`
+ then
+ echo "$SYSTEM_NAME"
+ else
+ echo rs6000-ibm-aix3.2.5
+ fi
+ elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then
+ echo rs6000-ibm-aix3.2.4
+ else
+ echo rs6000-ibm-aix3.2
+ fi
+ exit ;;
+ *:AIX:*:[4567])
+ IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'`
+ if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then
+ IBM_ARCH=rs6000
+ else
+ IBM_ARCH=powerpc
+ fi
+ if [ -x /usr/bin/lslpp ] ; then
+ IBM_REV=`/usr/bin/lslpp -Lqc bos.rte.libc |
+ awk -F: '{ print $3 }' | sed s/[0-9]*$/0/`
+ else
+ IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE}
+ fi
+ echo ${IBM_ARCH}-ibm-aix${IBM_REV}
+ exit ;;
+ *:AIX:*:*)
+ echo rs6000-ibm-aix
+ exit ;;
+ ibmrt:4.4BSD:*|romp-ibm:BSD:*)
+ echo romp-ibm-bsd4.4
+ exit ;;
+ ibmrt:*BSD:*|romp-ibm:BSD:*) # covers RT/PC BSD and
+ echo romp-ibm-bsd${UNAME_RELEASE} # 4.3 with uname added to
+ exit ;; # report: romp-ibm BSD 4.3
+ *:BOSX:*:*)
+ echo rs6000-bull-bosx
+ exit ;;
+ DPX/2?00:B.O.S.:*:*)
+ echo m68k-bull-sysv3
+ exit ;;
+ 9000/[34]??:4.3bsd:1.*:*)
+ echo m68k-hp-bsd
+ exit ;;
+ hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*)
+ echo m68k-hp-bsd4.4
+ exit ;;
+ 9000/[34678]??:HP-UX:*:*)
+ HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+ case "${UNAME_MACHINE}" in
+ 9000/31? ) HP_ARCH=m68000 ;;
+ 9000/[34]?? ) HP_ARCH=m68k ;;
+ 9000/[678][0-9][0-9])
+ if [ -x /usr/bin/getconf ]; then
+ sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null`
+ sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null`
+ case "${sc_cpu_version}" in
+ 523) HP_ARCH=hppa1.0 ;; # CPU_PA_RISC1_0
+ 528) HP_ARCH=hppa1.1 ;; # CPU_PA_RISC1_1
+ 532) # CPU_PA_RISC2_0
+ case "${sc_kernel_bits}" in
+ 32) HP_ARCH=hppa2.0n ;;
+ 64) HP_ARCH=hppa2.0w ;;
+ '') HP_ARCH=hppa2.0 ;; # HP-UX 10.20
+ esac ;;
+ esac
+ fi
+ if [ "${HP_ARCH}" = "" ]; then
+ eval $set_cc_for_build
+ sed 's/^ //' << EOF >$dummy.c
+
+ #define _HPUX_SOURCE
+ #include
+ #include
+
+ int main ()
+ {
+ #if defined(_SC_KERNEL_BITS)
+ long bits = sysconf(_SC_KERNEL_BITS);
+ #endif
+ long cpu = sysconf (_SC_CPU_VERSION);
+
+ switch (cpu)
+ {
+ case CPU_PA_RISC1_0: puts ("hppa1.0"); break;
+ case CPU_PA_RISC1_1: puts ("hppa1.1"); break;
+ case CPU_PA_RISC2_0:
+ #if defined(_SC_KERNEL_BITS)
+ switch (bits)
+ {
+ case 64: puts ("hppa2.0w"); break;
+ case 32: puts ("hppa2.0n"); break;
+ default: puts ("hppa2.0"); break;
+ } break;
+ #else /* !defined(_SC_KERNEL_BITS) */
+ puts ("hppa2.0"); break;
+ #endif
+ default: puts ("hppa1.0"); break;
+ }
+ exit (0);
+ }
+EOF
+ (CCOPTS="" $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null) && HP_ARCH=`$dummy`
+ test -z "$HP_ARCH" && HP_ARCH=hppa
+ fi ;;
+ esac
+ if [ ${HP_ARCH} = hppa2.0w ]
+ then
+ eval $set_cc_for_build
+
+ # hppa2.0w-hp-hpux* has a 64-bit kernel and a compiler generating
+ # 32-bit code. hppa64-hp-hpux* has the same kernel and a compiler
+ # generating 64-bit code. GNU and HP use different nomenclature:
+ #
+ # $ CC_FOR_BUILD=cc ./config.guess
+ # => hppa2.0w-hp-hpux11.23
+ # $ CC_FOR_BUILD="cc +DA2.0w" ./config.guess
+ # => hppa64-hp-hpux11.23
+
+ if echo __LP64__ | (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) |
+ grep -q __LP64__
+ then
+ HP_ARCH=hppa2.0w
+ else
+ HP_ARCH=hppa64
+ fi
+ fi
+ echo ${HP_ARCH}-hp-hpux${HPUX_REV}
+ exit ;;
+ ia64:HP-UX:*:*)
+ HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+ echo ia64-hp-hpux${HPUX_REV}
+ exit ;;
+ 3050*:HI-UX:*:*)
+ eval $set_cc_for_build
+ sed 's/^ //' << EOF >$dummy.c
+ #include
+ int
+ main ()
+ {
+ long cpu = sysconf (_SC_CPU_VERSION);
+ /* The order matters, because CPU_IS_HP_MC68K erroneously returns
+ true for CPU_PA_RISC1_0. CPU_IS_PA_RISC returns correct
+ results, however. */
+ if (CPU_IS_PA_RISC (cpu))
+ {
+ switch (cpu)
+ {
+ case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break;
+ case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break;
+ case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break;
+ default: puts ("hppa-hitachi-hiuxwe2"); break;
+ }
+ }
+ else if (CPU_IS_HP_MC68K (cpu))
+ puts ("m68k-hitachi-hiuxwe2");
+ else puts ("unknown-hitachi-hiuxwe2");
+ exit (0);
+ }
+EOF
+ $CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy` &&
+ { echo "$SYSTEM_NAME"; exit; }
+ echo unknown-hitachi-hiuxwe2
+ exit ;;
+ 9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* )
+ echo hppa1.1-hp-bsd
+ exit ;;
+ 9000/8??:4.3bsd:*:*)
+ echo hppa1.0-hp-bsd
+ exit ;;
+ *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*)
+ echo hppa1.0-hp-mpeix
+ exit ;;
+ hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* )
+ echo hppa1.1-hp-osf
+ exit ;;
+ hp8??:OSF1:*:*)
+ echo hppa1.0-hp-osf
+ exit ;;
+ i*86:OSF1:*:*)
+ if [ -x /usr/sbin/sysversion ] ; then
+ echo ${UNAME_MACHINE}-unknown-osf1mk
+ else
+ echo ${UNAME_MACHINE}-unknown-osf1
+ fi
+ exit ;;
+ parisc*:Lites*:*:*)
+ echo hppa1.1-hp-lites
+ exit ;;
+ C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*)
+ echo c1-convex-bsd
+ exit ;;
+ C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*)
+ if getsysinfo -f scalar_acc
+ then echo c32-convex-bsd
+ else echo c2-convex-bsd
+ fi
+ exit ;;
+ C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*)
+ echo c34-convex-bsd
+ exit ;;
+ C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*)
+ echo c38-convex-bsd
+ exit ;;
+ C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*)
+ echo c4-convex-bsd
+ exit ;;
+ CRAY*Y-MP:*:*:*)
+ echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+ exit ;;
+ CRAY*[A-Z]90:*:*:*)
+ echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \
+ | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \
+ -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \
+ -e 's/\.[^.]*$/.X/'
+ exit ;;
+ CRAY*TS:*:*:*)
+ echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+ exit ;;
+ CRAY*T3E:*:*:*)
+ echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+ exit ;;
+ CRAY*SV1:*:*:*)
+ echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+ exit ;;
+ *:UNICOS/mp:*:*)
+ echo craynv-cray-unicosmp${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+ exit ;;
+ F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*)
+ FUJITSU_PROC=`uname -m | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz`
+ FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'`
+ FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'`
+ echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}"
+ exit ;;
+ 5000:UNIX_System_V:4.*:*)
+ FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'`
+ FUJITSU_REL=`echo ${UNAME_RELEASE} | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/ /_/'`
+ echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}"
+ exit ;;
+ i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*)
+ echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE}
+ exit ;;
+ sparc*:BSD/OS:*:*)
+ echo sparc-unknown-bsdi${UNAME_RELEASE}
+ exit ;;
+ *:BSD/OS:*:*)
+ echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE}
+ exit ;;
+ *:FreeBSD:*:*)
+ UNAME_PROCESSOR=`/usr/bin/uname -p`
+ case ${UNAME_PROCESSOR} in
+ amd64)
+ echo x86_64-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;;
+ *)
+ echo ${UNAME_PROCESSOR}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;;
+ esac
+ exit ;;
+ i*:CYGWIN*:*)
+ echo ${UNAME_MACHINE}-pc-cygwin
+ exit ;;
+ *:MINGW64*:*)
+ echo ${UNAME_MACHINE}-pc-mingw64
+ exit ;;
+ *:MINGW*:*)
+ echo ${UNAME_MACHINE}-pc-mingw32
+ exit ;;
+ *:MSYS*:*)
+ echo ${UNAME_MACHINE}-pc-msys
+ exit ;;
+ i*:windows32*:*)
+ # uname -m includes "-pc" on this system.
+ echo ${UNAME_MACHINE}-mingw32
+ exit ;;
+ i*:PW*:*)
+ echo ${UNAME_MACHINE}-pc-pw32
+ exit ;;
+ *:Interix*:*)
+ case ${UNAME_MACHINE} in
+ x86)
+ echo i586-pc-interix${UNAME_RELEASE}
+ exit ;;
+ authenticamd | genuineintel | EM64T)
+ echo x86_64-unknown-interix${UNAME_RELEASE}
+ exit ;;
+ IA64)
+ echo ia64-unknown-interix${UNAME_RELEASE}
+ exit ;;
+ esac ;;
+ [345]86:Windows_95:* | [345]86:Windows_98:* | [345]86:Windows_NT:*)
+ echo i${UNAME_MACHINE}-pc-mks
+ exit ;;
+ 8664:Windows_NT:*)
+ echo x86_64-pc-mks
+ exit ;;
+ i*:Windows_NT*:* | Pentium*:Windows_NT*:*)
+ # How do we know it's Interix rather than the generic POSIX subsystem?
+ # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we
+ # UNAME_MACHINE based on the output of uname instead of i386?
+ echo i586-pc-interix
+ exit ;;
+ i*:UWIN*:*)
+ echo ${UNAME_MACHINE}-pc-uwin
+ exit ;;
+ amd64:CYGWIN*:*:* | x86_64:CYGWIN*:*:*)
+ echo x86_64-unknown-cygwin
+ exit ;;
+ p*:CYGWIN*:*)
+ echo powerpcle-unknown-cygwin
+ exit ;;
+ prep*:SunOS:5.*:*)
+ echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+ exit ;;
+ *:GNU:*:*)
+ # the GNU system
+ echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-${LIBC}`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'`
+ exit ;;
+ *:GNU/*:*:*)
+ # other systems with GNU libc and userland
+ echo ${UNAME_MACHINE}-unknown-`echo ${UNAME_SYSTEM} | sed 's,^[^/]*/,,' | tr "[:upper:]" "[:lower:]"``echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`-${LIBC}
+ exit ;;
+ i*86:Minix:*:*)
+ echo ${UNAME_MACHINE}-pc-minix
+ exit ;;
+ aarch64:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ aarch64_be:Linux:*:*)
+ UNAME_MACHINE=aarch64_be
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ alpha:Linux:*:*)
+ case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in
+ EV5) UNAME_MACHINE=alphaev5 ;;
+ EV56) UNAME_MACHINE=alphaev56 ;;
+ PCA56) UNAME_MACHINE=alphapca56 ;;
+ PCA57) UNAME_MACHINE=alphapca56 ;;
+ EV6) UNAME_MACHINE=alphaev6 ;;
+ EV67) UNAME_MACHINE=alphaev67 ;;
+ EV68*) UNAME_MACHINE=alphaev68 ;;
+ esac
+ objdump --private-headers /bin/sh | grep -q ld.so.1
+ if test "$?" = 0 ; then LIBC=gnulibc1 ; fi
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ arc:Linux:*:* | arceb:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ arm*:Linux:*:*)
+ eval $set_cc_for_build
+ if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \
+ | grep -q __ARM_EABI__
+ then
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ else
+ if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \
+ | grep -q __ARM_PCS_VFP
+ then
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}eabi
+ else
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}eabihf
+ fi
+ fi
+ exit ;;
+ avr32*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ cris:Linux:*:*)
+ echo ${UNAME_MACHINE}-axis-linux-${LIBC}
+ exit ;;
+ crisv32:Linux:*:*)
+ echo ${UNAME_MACHINE}-axis-linux-${LIBC}
+ exit ;;
+ e2k:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ frv:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ hexagon:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ i*86:Linux:*:*)
+ echo ${UNAME_MACHINE}-pc-linux-${LIBC}
+ exit ;;
+ ia64:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ k1om:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ m32r*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ m68*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ mips:Linux:*:* | mips64:Linux:*:*)
+ eval $set_cc_for_build
+ sed 's/^ //' << EOF >$dummy.c
+ #undef CPU
+ #undef ${UNAME_MACHINE}
+ #undef ${UNAME_MACHINE}el
+ #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL)
+ CPU=${UNAME_MACHINE}el
+ #else
+ #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB)
+ CPU=${UNAME_MACHINE}
+ #else
+ CPU=
+ #endif
+ #endif
+EOF
+ eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^CPU'`
+ test x"${CPU}" != x && { echo "${CPU}-unknown-linux-${LIBC}"; exit; }
+ ;;
+ mips64el:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ openrisc*:Linux:*:*)
+ echo or1k-unknown-linux-${LIBC}
+ exit ;;
+ or32:Linux:*:* | or1k*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ padre:Linux:*:*)
+ echo sparc-unknown-linux-${LIBC}
+ exit ;;
+ parisc64:Linux:*:* | hppa64:Linux:*:*)
+ echo hppa64-unknown-linux-${LIBC}
+ exit ;;
+ parisc:Linux:*:* | hppa:Linux:*:*)
+ # Look for CPU level
+ case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in
+ PA7*) echo hppa1.1-unknown-linux-${LIBC} ;;
+ PA8*) echo hppa2.0-unknown-linux-${LIBC} ;;
+ *) echo hppa-unknown-linux-${LIBC} ;;
+ esac
+ exit ;;
+ ppc64:Linux:*:*)
+ echo powerpc64-unknown-linux-${LIBC}
+ exit ;;
+ ppc:Linux:*:*)
+ echo powerpc-unknown-linux-${LIBC}
+ exit ;;
+ ppc64le:Linux:*:*)
+ echo powerpc64le-unknown-linux-${LIBC}
+ exit ;;
+ ppcle:Linux:*:*)
+ echo powerpcle-unknown-linux-${LIBC}
+ exit ;;
+ riscv32:Linux:*:* | riscv64:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ s390:Linux:*:* | s390x:Linux:*:*)
+ echo ${UNAME_MACHINE}-ibm-linux-${LIBC}
+ exit ;;
+ sh64*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ sh*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ sparc:Linux:*:* | sparc64:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ tile*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ vax:Linux:*:*)
+ echo ${UNAME_MACHINE}-dec-linux-${LIBC}
+ exit ;;
+ x86_64:Linux:*:*)
+ echo ${UNAME_MACHINE}-pc-linux-${LIBC}
+ exit ;;
+ xtensa*:Linux:*:*)
+ echo ${UNAME_MACHINE}-unknown-linux-${LIBC}
+ exit ;;
+ i*86:DYNIX/ptx:4*:*)
+ # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there.
+ # earlier versions are messed up and put the nodename in both
+ # sysname and nodename.
+ echo i386-sequent-sysv4
+ exit ;;
+ i*86:UNIX_SV:4.2MP:2.*)
+ # Unixware is an offshoot of SVR4, but it has its own version
+ # number series starting with 2...
+ # I am not positive that other SVR4 systems won't match this,
+ # I just have to hope. -- rms.
+ # Use sysv4.2uw... so that sysv4* matches it.
+ echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION}
+ exit ;;
+ i*86:OS/2:*:*)
+ # If we were able to find `uname', then EMX Unix compatibility
+ # is probably installed.
+ echo ${UNAME_MACHINE}-pc-os2-emx
+ exit ;;
+ i*86:XTS-300:*:STOP)
+ echo ${UNAME_MACHINE}-unknown-stop
+ exit ;;
+ i*86:atheos:*:*)
+ echo ${UNAME_MACHINE}-unknown-atheos
+ exit ;;
+ i*86:syllable:*:*)
+ echo ${UNAME_MACHINE}-pc-syllable
+ exit ;;
+ i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*)
+ echo i386-unknown-lynxos${UNAME_RELEASE}
+ exit ;;
+ i*86:*DOS:*:*)
+ echo ${UNAME_MACHINE}-pc-msdosdjgpp
+ exit ;;
+ i*86:*:4.*:* | i*86:SYSTEM_V:4.*:*)
+ UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'`
+ if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then
+ echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL}
+ else
+ echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL}
+ fi
+ exit ;;
+ i*86:*:5:[678]*)
+ # UnixWare 7.x, OpenUNIX and OpenServer 6.
+ case `/bin/uname -X | grep "^Machine"` in
+ *486*) UNAME_MACHINE=i486 ;;
+ *Pentium) UNAME_MACHINE=i586 ;;
+ *Pent*|*Celeron) UNAME_MACHINE=i686 ;;
+ esac
+ echo ${UNAME_MACHINE}-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION}
+ exit ;;
+ i*86:*:3.2:*)
+ if test -f /usr/options/cb.name; then
+ UNAME_REL=`sed -n 's/.*Version //p' /dev/null >/dev/null ; then
+ UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')`
+ (/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486
+ (/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \
+ && UNAME_MACHINE=i586
+ (/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \
+ && UNAME_MACHINE=i686
+ (/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \
+ && UNAME_MACHINE=i686
+ echo ${UNAME_MACHINE}-pc-sco$UNAME_REL
+ else
+ echo ${UNAME_MACHINE}-pc-sysv32
+ fi
+ exit ;;
+ pc:*:*:*)
+ # Left here for compatibility:
+ # uname -m prints for DJGPP always 'pc', but it prints nothing about
+ # the processor, so we play safe by assuming i586.
+ # Note: whatever this is, it MUST be the same as what config.sub
+ # prints for the "djgpp" host, or else GDB configure will decide that
+ # this is a cross-build.
+ echo i586-pc-msdosdjgpp
+ exit ;;
+ Intel:Mach:3*:*)
+ echo i386-pc-mach3
+ exit ;;
+ paragon:*:*:*)
+ echo i860-intel-osf1
+ exit ;;
+ i860:*:4.*:*) # i860-SVR4
+ if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then
+ echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4
+ else # Add other i860-SVR4 vendors below as they are discovered.
+ echo i860-unknown-sysv${UNAME_RELEASE} # Unknown i860-SVR4
+ fi
+ exit ;;
+ mini*:CTIX:SYS*5:*)
+ # "miniframe"
+ echo m68010-convergent-sysv
+ exit ;;
+ mc68k:UNIX:SYSTEM5:3.51m)
+ echo m68k-convergent-sysv
+ exit ;;
+ M680?0:D-NIX:5.3:*)
+ echo m68k-diab-dnix
+ exit ;;
+ M68*:*:R3V[5678]*:*)
+ test -r /sysV68 && { echo 'm68k-motorola-sysv'; exit; } ;;
+ 3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0)
+ OS_REL=''
+ test -r /etc/.relid \
+ && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+ /bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+ && { echo i486-ncr-sysv4.3${OS_REL}; exit; }
+ /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+ && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;;
+ 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*)
+ /bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+ && { echo i486-ncr-sysv4; exit; } ;;
+ NCR*:*:4.2:* | MPRAS*:*:4.2:*)
+ OS_REL='.3'
+ test -r /etc/.relid \
+ && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+ /bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+ && { echo i486-ncr-sysv4.3${OS_REL}; exit; }
+ /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+ && { echo i586-ncr-sysv4.3${OS_REL}; exit; }
+ /bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \
+ && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;;
+ m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*)
+ echo m68k-unknown-lynxos${UNAME_RELEASE}
+ exit ;;
+ mc68030:UNIX_System_V:4.*:*)
+ echo m68k-atari-sysv4
+ exit ;;
+ TSUNAMI:LynxOS:2.*:*)
+ echo sparc-unknown-lynxos${UNAME_RELEASE}
+ exit ;;
+ rs6000:LynxOS:2.*:*)
+ echo rs6000-unknown-lynxos${UNAME_RELEASE}
+ exit ;;
+ PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*)
+ echo powerpc-unknown-lynxos${UNAME_RELEASE}
+ exit ;;
+ SM[BE]S:UNIX_SV:*:*)
+ echo mips-dde-sysv${UNAME_RELEASE}
+ exit ;;
+ RM*:ReliantUNIX-*:*:*)
+ echo mips-sni-sysv4
+ exit ;;
+ RM*:SINIX-*:*:*)
+ echo mips-sni-sysv4
+ exit ;;
+ *:SINIX-*:*:*)
+ if uname -p 2>/dev/null >/dev/null ; then
+ UNAME_MACHINE=`(uname -p) 2>/dev/null`
+ echo ${UNAME_MACHINE}-sni-sysv4
+ else
+ echo ns32k-sni-sysv
+ fi
+ exit ;;
+ PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort
+ # says
+ echo i586-unisys-sysv4
+ exit ;;
+ *:UNIX_System_V:4*:FTX*)
+ # From Gerald Hewes .
+ # How about differentiating between stratus architectures? -djm
+ echo hppa1.1-stratus-sysv4
+ exit ;;
+ *:*:*:FTX*)
+ # From seanf@swdc.stratus.com.
+ echo i860-stratus-sysv4
+ exit ;;
+ i*86:VOS:*:*)
+ # From Paul.Green@stratus.com.
+ echo ${UNAME_MACHINE}-stratus-vos
+ exit ;;
+ *:VOS:*:*)
+ # From Paul.Green@stratus.com.
+ echo hppa1.1-stratus-vos
+ exit ;;
+ mc68*:A/UX:*:*)
+ echo m68k-apple-aux${UNAME_RELEASE}
+ exit ;;
+ news*:NEWS-OS:6*:*)
+ echo mips-sony-newsos6
+ exit ;;
+ R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*)
+ if [ -d /usr/nec ]; then
+ echo mips-nec-sysv${UNAME_RELEASE}
+ else
+ echo mips-unknown-sysv${UNAME_RELEASE}
+ fi
+ exit ;;
+ BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only.
+ echo powerpc-be-beos
+ exit ;;
+ BeMac:BeOS:*:*) # BeOS running on Mac or Mac clone, PPC only.
+ echo powerpc-apple-beos
+ exit ;;
+ BePC:BeOS:*:*) # BeOS running on Intel PC compatible.
+ echo i586-pc-beos
+ exit ;;
+ BePC:Haiku:*:*) # Haiku running on Intel PC compatible.
+ echo i586-pc-haiku
+ exit ;;
+ x86_64:Haiku:*:*)
+ echo x86_64-unknown-haiku
+ exit ;;
+ SX-4:SUPER-UX:*:*)
+ echo sx4-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-5:SUPER-UX:*:*)
+ echo sx5-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-6:SUPER-UX:*:*)
+ echo sx6-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-7:SUPER-UX:*:*)
+ echo sx7-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-8:SUPER-UX:*:*)
+ echo sx8-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-8R:SUPER-UX:*:*)
+ echo sx8r-nec-superux${UNAME_RELEASE}
+ exit ;;
+ SX-ACE:SUPER-UX:*:*)
+ echo sxace-nec-superux${UNAME_RELEASE}
+ exit ;;
+ Power*:Rhapsody:*:*)
+ echo powerpc-apple-rhapsody${UNAME_RELEASE}
+ exit ;;
+ *:Rhapsody:*:*)
+ echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE}
+ exit ;;
+ *:Darwin:*:*)
+ UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown
+ eval $set_cc_for_build
+ if test "$UNAME_PROCESSOR" = unknown ; then
+ UNAME_PROCESSOR=powerpc
+ fi
+ if test `echo "$UNAME_RELEASE" | sed -e 's/\..*//'` -le 10 ; then
+ if [ "$CC_FOR_BUILD" != no_compiler_found ]; then
+ if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \
+ (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \
+ grep IS_64BIT_ARCH >/dev/null
+ then
+ case $UNAME_PROCESSOR in
+ i386) UNAME_PROCESSOR=x86_64 ;;
+ powerpc) UNAME_PROCESSOR=powerpc64 ;;
+ esac
+ fi
+ fi
+ elif test "$UNAME_PROCESSOR" = i386 ; then
+ # Avoid executing cc on OS X 10.9, as it ships with a stub
+ # that puts up a graphical alert prompting to install
+ # developer tools. Any system running Mac OS X 10.7 or
+ # later (Darwin 11 and later) is required to have a 64-bit
+ # processor. This is not true of the ARM version of Darwin
+ # that Apple uses in portable devices.
+ UNAME_PROCESSOR=x86_64
+ fi
+ echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE}
+ exit ;;
+ *:procnto*:*:* | *:QNX:[0123456789]*:*)
+ UNAME_PROCESSOR=`uname -p`
+ if test "$UNAME_PROCESSOR" = x86; then
+ UNAME_PROCESSOR=i386
+ UNAME_MACHINE=pc
+ fi
+ echo ${UNAME_PROCESSOR}-${UNAME_MACHINE}-nto-qnx${UNAME_RELEASE}
+ exit ;;
+ *:QNX:*:4*)
+ echo i386-pc-qnx
+ exit ;;
+ NEO-?:NONSTOP_KERNEL:*:*)
+ echo neo-tandem-nsk${UNAME_RELEASE}
+ exit ;;
+ NSE-*:NONSTOP_KERNEL:*:*)
+ echo nse-tandem-nsk${UNAME_RELEASE}
+ exit ;;
+ NSR-?:NONSTOP_KERNEL:*:*)
+ echo nsr-tandem-nsk${UNAME_RELEASE}
+ exit ;;
+ *:NonStop-UX:*:*)
+ echo mips-compaq-nonstopux
+ exit ;;
+ BS2000:POSIX*:*:*)
+ echo bs2000-siemens-sysv
+ exit ;;
+ DS/*:UNIX_System_V:*:*)
+ echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE}
+ exit ;;
+ *:Plan9:*:*)
+ # "uname -m" is not consistent, so use $cputype instead. 386
+ # is converted to i386 for consistency with other x86
+ # operating systems.
+ if test "$cputype" = 386; then
+ UNAME_MACHINE=i386
+ else
+ UNAME_MACHINE="$cputype"
+ fi
+ echo ${UNAME_MACHINE}-unknown-plan9
+ exit ;;
+ *:TOPS-10:*:*)
+ echo pdp10-unknown-tops10
+ exit ;;
+ *:TENEX:*:*)
+ echo pdp10-unknown-tenex
+ exit ;;
+ KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*)
+ echo pdp10-dec-tops20
+ exit ;;
+ XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*)
+ echo pdp10-xkl-tops20
+ exit ;;
+ *:TOPS-20:*:*)
+ echo pdp10-unknown-tops20
+ exit ;;
+ *:ITS:*:*)
+ echo pdp10-unknown-its
+ exit ;;
+ SEI:*:*:SEIUX)
+ echo mips-sei-seiux${UNAME_RELEASE}
+ exit ;;
+ *:DragonFly:*:*)
+ echo ${UNAME_MACHINE}-unknown-dragonfly`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`
+ exit ;;
+ *:*VMS:*:*)
+ UNAME_MACHINE=`(uname -p) 2>/dev/null`
+ case "${UNAME_MACHINE}" in
+ A*) echo alpha-dec-vms ; exit ;;
+ I*) echo ia64-dec-vms ; exit ;;
+ V*) echo vax-dec-vms ; exit ;;
+ esac ;;
+ *:XENIX:*:SysV)
+ echo i386-pc-xenix
+ exit ;;
+ i*86:skyos:*:*)
+ echo ${UNAME_MACHINE}-pc-skyos`echo ${UNAME_RELEASE} | sed -e 's/ .*$//'`
+ exit ;;
+ i*86:rdos:*:*)
+ echo ${UNAME_MACHINE}-pc-rdos
+ exit ;;
+ i*86:AROS:*:*)
+ echo ${UNAME_MACHINE}-pc-aros
+ exit ;;
+ x86_64:VMkernel:*:*)
+ echo ${UNAME_MACHINE}-unknown-esx
+ exit ;;
+ amd64:Isilon\ OneFS:*:*)
+ echo x86_64-unknown-onefs
+ exit ;;
+esac
+
+cat >&2 </dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null`
+/bin/uname -X = `(/bin/uname -X) 2>/dev/null`
+
+hostinfo = `(hostinfo) 2>/dev/null`
+/bin/universe = `(/bin/universe) 2>/dev/null`
+/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null`
+/bin/arch = `(/bin/arch) 2>/dev/null`
+/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null`
+
+UNAME_MACHINE = ${UNAME_MACHINE}
+UNAME_RELEASE = ${UNAME_RELEASE}
+UNAME_SYSTEM = ${UNAME_SYSTEM}
+UNAME_VERSION = ${UNAME_VERSION}
+EOF
+
+exit 1
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/debian/.debhelper/bucket/index b/debian/.debhelper/bucket/index
new file mode 100644
index 0000000..fa26844
--- /dev/null
+++ b/debian/.debhelper/bucket/index
@@ -0,0 +1,2 @@
+8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27 ext/libpqxx-7.7.3/config/config.guess
+1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f ext/libpqxx-7.7.3/config/config.sub
diff --git a/debian/.debhelper/generated/backone-addon-chat/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/backone-addon-chat/dh_installchangelogs.dch.trimmed
new file mode 100644
index 0000000..e11e45c
--- /dev/null
+++ b/debian/.debhelper/generated/backone-addon-chat/dh_installchangelogs.dch.trimmed
@@ -0,0 +1,257 @@
+backone (1.17.1) stable; urgency=medium
+
+ * Fix /api/backone routes and reinstallable Web UI packages
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 12:02:07 +0700
+
+backone (1.17.0) stable; urgency=medium
+
+ * Tray app, auto web server, peer contact UI
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 09:55:07 +0700
+
+backone (1.16.0) stable; urgency=medium
+
+ * Release notes
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:59:49 +0700
+
+backone (1.15.0) stable; urgency=medium
+
+ * Separated addon architecture: voice, video, chat, updater
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:37:16 +0700
+zerotier-one (1.14.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 11 Sep 2024 01:00:00 -0700
+
+zerotier-one (1.14.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 19 Mar 2024 01:00:00 -0700
+
+zerotier-one (1.12.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 12 Sep 2023 01:00:00 -0700
+
+zerotier-one (1.12.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 25 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.12.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 17 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.10.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 21 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 10 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 06 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Sat, 21 Jan 2023 01:00:00 -0700
+
+zerotier-one (1.10.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 13 Oct 2022 01:00:00 -0700
+
+zerotier-one (1.10.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 27 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.10.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 11 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.7) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 21 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 07 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 17 Dec 2021 01:00:00 -0700
+
+zerotier-one (1.8.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 23 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 15 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 08 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 20 Oct 2021 01:00:00 -0700
+
+zerotier-one (1.8.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 15 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.6) unstable; urgency=medium
+
+ * Backport endpoint mitigation against address collision attack.
+
+ -- Adam Ierymenko Tue, 21 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.5) unstable; urgency=medium
+
+ * Fix path filtering bug that could cause "software laser" effect.
+ * Fix printf overflow in CLI (not exploitable or security related)
+ * Fix Windows device enumeration issue.
+
+ -- Adam Ierymenko Tue, 13 Apr 2021 01:00:00 -0700
+
+zerotier-one (1.6.4) unstable; urgency=medium
+
+ * REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 15 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.3-1) unstable; urgency=medium
+
+ * Fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 02 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.2-2) unstable; urgency=medium
+
+ * This is a minor update to the 1.6.2 package to address issues with
+ running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
+
+ -- Adam Ierymenko Tue, 31 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 30 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 24 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 19 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.5.0) unstable; urgency=medium
+
+ * Version 1.5.0 is actually 1.6.0-beta1
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 05 Aug 2020 01:00:00 -0700
+
+zerotier-one (1.4.6) unstable; urgency=medium
+
+ * Update default root server list
+ * Fix build flags on "armhf" (32-bit ARM) platforms for better
+ compatibility with Pi Zero and other devices.
+ * Fix license text in one.cpp.
+ * Add a clarification to LICENSE.txt.
+
+ -- Adam Ierymenko Fri, 30 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.4) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * License changed to BSL 1.1
+
+ -- Adam Ierymenko Fri, 23 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2-2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * This is a new build that fixes a binary build issue with containers and SELinux
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.0) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 29 Jul 2019 01:00:00 -0700
+
+# Older entries have been removed from this changelog.
+# To read the complete changelog use `apt changelog backone-addon-chat`.
diff --git a/debian/.debhelper/generated/backone-addon-chat/installed-by-dh_installman b/debian/.debhelper/generated/backone-addon-chat/installed-by-dh_installman
new file mode 100644
index 0000000..e69de29
diff --git a/debian/.debhelper/generated/backone-addon-comms-core/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/backone-addon-comms-core/dh_installchangelogs.dch.trimmed
new file mode 100644
index 0000000..b138f51
--- /dev/null
+++ b/debian/.debhelper/generated/backone-addon-comms-core/dh_installchangelogs.dch.trimmed
@@ -0,0 +1,257 @@
+backone (1.17.1) stable; urgency=medium
+
+ * Fix /api/backone routes and reinstallable Web UI packages
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 12:02:07 +0700
+
+backone (1.17.0) stable; urgency=medium
+
+ * Tray app, auto web server, peer contact UI
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 09:55:07 +0700
+
+backone (1.16.0) stable; urgency=medium
+
+ * Release notes
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:59:49 +0700
+
+backone (1.15.0) stable; urgency=medium
+
+ * Separated addon architecture: voice, video, chat, updater
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:37:16 +0700
+zerotier-one (1.14.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 11 Sep 2024 01:00:00 -0700
+
+zerotier-one (1.14.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 19 Mar 2024 01:00:00 -0700
+
+zerotier-one (1.12.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 12 Sep 2023 01:00:00 -0700
+
+zerotier-one (1.12.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 25 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.12.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 17 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.10.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 21 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 10 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 06 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Sat, 21 Jan 2023 01:00:00 -0700
+
+zerotier-one (1.10.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 13 Oct 2022 01:00:00 -0700
+
+zerotier-one (1.10.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 27 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.10.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 11 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.7) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 21 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 07 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 17 Dec 2021 01:00:00 -0700
+
+zerotier-one (1.8.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 23 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 15 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 08 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 20 Oct 2021 01:00:00 -0700
+
+zerotier-one (1.8.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 15 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.6) unstable; urgency=medium
+
+ * Backport endpoint mitigation against address collision attack.
+
+ -- Adam Ierymenko Tue, 21 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.5) unstable; urgency=medium
+
+ * Fix path filtering bug that could cause "software laser" effect.
+ * Fix printf overflow in CLI (not exploitable or security related)
+ * Fix Windows device enumeration issue.
+
+ -- Adam Ierymenko Tue, 13 Apr 2021 01:00:00 -0700
+
+zerotier-one (1.6.4) unstable; urgency=medium
+
+ * REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 15 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.3-1) unstable; urgency=medium
+
+ * Fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 02 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.2-2) unstable; urgency=medium
+
+ * This is a minor update to the 1.6.2 package to address issues with
+ running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
+
+ -- Adam Ierymenko Tue, 31 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 30 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 24 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 19 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.5.0) unstable; urgency=medium
+
+ * Version 1.5.0 is actually 1.6.0-beta1
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 05 Aug 2020 01:00:00 -0700
+
+zerotier-one (1.4.6) unstable; urgency=medium
+
+ * Update default root server list
+ * Fix build flags on "armhf" (32-bit ARM) platforms for better
+ compatibility with Pi Zero and other devices.
+ * Fix license text in one.cpp.
+ * Add a clarification to LICENSE.txt.
+
+ -- Adam Ierymenko Fri, 30 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.4) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * License changed to BSL 1.1
+
+ -- Adam Ierymenko Fri, 23 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2-2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * This is a new build that fixes a binary build issue with containers and SELinux
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.0) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 29 Jul 2019 01:00:00 -0700
+
+# Older entries have been removed from this changelog.
+# To read the complete changelog use `apt changelog backone-addon-comms-core`.
diff --git a/debian/.debhelper/generated/backone-addon-comms-core/installed-by-dh_installman b/debian/.debhelper/generated/backone-addon-comms-core/installed-by-dh_installman
new file mode 100644
index 0000000..e69de29
diff --git a/debian/.debhelper/generated/backone-addon-updater/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/backone-addon-updater/dh_installchangelogs.dch.trimmed
new file mode 100644
index 0000000..973da32
--- /dev/null
+++ b/debian/.debhelper/generated/backone-addon-updater/dh_installchangelogs.dch.trimmed
@@ -0,0 +1,257 @@
+backone (1.17.1) stable; urgency=medium
+
+ * Fix /api/backone routes and reinstallable Web UI packages
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 12:02:07 +0700
+
+backone (1.17.0) stable; urgency=medium
+
+ * Tray app, auto web server, peer contact UI
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 09:55:07 +0700
+
+backone (1.16.0) stable; urgency=medium
+
+ * Release notes
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:59:49 +0700
+
+backone (1.15.0) stable; urgency=medium
+
+ * Separated addon architecture: voice, video, chat, updater
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:37:16 +0700
+zerotier-one (1.14.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 11 Sep 2024 01:00:00 -0700
+
+zerotier-one (1.14.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 19 Mar 2024 01:00:00 -0700
+
+zerotier-one (1.12.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 12 Sep 2023 01:00:00 -0700
+
+zerotier-one (1.12.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 25 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.12.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 17 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.10.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 21 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 10 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 06 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Sat, 21 Jan 2023 01:00:00 -0700
+
+zerotier-one (1.10.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 13 Oct 2022 01:00:00 -0700
+
+zerotier-one (1.10.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 27 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.10.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 11 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.7) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 21 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 07 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 17 Dec 2021 01:00:00 -0700
+
+zerotier-one (1.8.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 23 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 15 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 08 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 20 Oct 2021 01:00:00 -0700
+
+zerotier-one (1.8.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 15 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.6) unstable; urgency=medium
+
+ * Backport endpoint mitigation against address collision attack.
+
+ -- Adam Ierymenko Tue, 21 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.5) unstable; urgency=medium
+
+ * Fix path filtering bug that could cause "software laser" effect.
+ * Fix printf overflow in CLI (not exploitable or security related)
+ * Fix Windows device enumeration issue.
+
+ -- Adam Ierymenko Tue, 13 Apr 2021 01:00:00 -0700
+
+zerotier-one (1.6.4) unstable; urgency=medium
+
+ * REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 15 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.3-1) unstable; urgency=medium
+
+ * Fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 02 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.2-2) unstable; urgency=medium
+
+ * This is a minor update to the 1.6.2 package to address issues with
+ running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
+
+ -- Adam Ierymenko Tue, 31 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 30 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 24 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 19 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.5.0) unstable; urgency=medium
+
+ * Version 1.5.0 is actually 1.6.0-beta1
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 05 Aug 2020 01:00:00 -0700
+
+zerotier-one (1.4.6) unstable; urgency=medium
+
+ * Update default root server list
+ * Fix build flags on "armhf" (32-bit ARM) platforms for better
+ compatibility with Pi Zero and other devices.
+ * Fix license text in one.cpp.
+ * Add a clarification to LICENSE.txt.
+
+ -- Adam Ierymenko Fri, 30 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.4) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * License changed to BSL 1.1
+
+ -- Adam Ierymenko Fri, 23 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2-2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * This is a new build that fixes a binary build issue with containers and SELinux
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.0) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 29 Jul 2019 01:00:00 -0700
+
+# Older entries have been removed from this changelog.
+# To read the complete changelog use `apt changelog backone-addon-updater`.
diff --git a/debian/.debhelper/generated/backone-addon-updater/installed-by-dh_installman b/debian/.debhelper/generated/backone-addon-updater/installed-by-dh_installman
new file mode 100644
index 0000000..e69de29
diff --git a/debian/.debhelper/generated/backone-addon-video/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/backone-addon-video/dh_installchangelogs.dch.trimmed
new file mode 100644
index 0000000..ce6cec7
--- /dev/null
+++ b/debian/.debhelper/generated/backone-addon-video/dh_installchangelogs.dch.trimmed
@@ -0,0 +1,257 @@
+backone (1.17.1) stable; urgency=medium
+
+ * Fix /api/backone routes and reinstallable Web UI packages
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 12:02:07 +0700
+
+backone (1.17.0) stable; urgency=medium
+
+ * Tray app, auto web server, peer contact UI
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 09:55:07 +0700
+
+backone (1.16.0) stable; urgency=medium
+
+ * Release notes
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:59:49 +0700
+
+backone (1.15.0) stable; urgency=medium
+
+ * Separated addon architecture: voice, video, chat, updater
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:37:16 +0700
+zerotier-one (1.14.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 11 Sep 2024 01:00:00 -0700
+
+zerotier-one (1.14.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 19 Mar 2024 01:00:00 -0700
+
+zerotier-one (1.12.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 12 Sep 2023 01:00:00 -0700
+
+zerotier-one (1.12.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 25 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.12.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 17 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.10.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 21 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 10 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 06 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Sat, 21 Jan 2023 01:00:00 -0700
+
+zerotier-one (1.10.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 13 Oct 2022 01:00:00 -0700
+
+zerotier-one (1.10.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 27 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.10.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 11 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.7) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 21 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 07 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 17 Dec 2021 01:00:00 -0700
+
+zerotier-one (1.8.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 23 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 15 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 08 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 20 Oct 2021 01:00:00 -0700
+
+zerotier-one (1.8.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 15 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.6) unstable; urgency=medium
+
+ * Backport endpoint mitigation against address collision attack.
+
+ -- Adam Ierymenko Tue, 21 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.5) unstable; urgency=medium
+
+ * Fix path filtering bug that could cause "software laser" effect.
+ * Fix printf overflow in CLI (not exploitable or security related)
+ * Fix Windows device enumeration issue.
+
+ -- Adam Ierymenko Tue, 13 Apr 2021 01:00:00 -0700
+
+zerotier-one (1.6.4) unstable; urgency=medium
+
+ * REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 15 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.3-1) unstable; urgency=medium
+
+ * Fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko Tue, 02 Feb 2021 01:00:00 -0700
+
+zerotier-one (1.6.2-2) unstable; urgency=medium
+
+ * This is a minor update to the 1.6.2 package to address issues with
+ running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
+
+ -- Adam Ierymenko Tue, 31 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 30 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 24 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.6.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 19 Nov 2020 01:00:00 -0700
+
+zerotier-one (1.5.0) unstable; urgency=medium
+
+ * Version 1.5.0 is actually 1.6.0-beta1
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 05 Aug 2020 01:00:00 -0700
+
+zerotier-one (1.4.6) unstable; urgency=medium
+
+ * Update default root server list
+ * Fix build flags on "armhf" (32-bit ARM) platforms for better
+ compatibility with Pi Zero and other devices.
+ * Fix license text in one.cpp.
+ * Add a clarification to LICENSE.txt.
+
+ -- Adam Ierymenko Fri, 30 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.4) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * License changed to BSL 1.1
+
+ -- Adam Ierymenko Fri, 23 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2-2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+ * This is a new build that fixes a binary build issue with containers and SELinux
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.2) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 04 Aug 2019 01:00:00 -0700
+
+zerotier-one (1.4.0) unstable; urgency=medium
+
+ * See https://github.com/zerotier/ZeroTierOne for release notes.
+
+ -- Adam Ierymenko Thu, 29 Jul 2019 01:00:00 -0700
+
+# Older entries have been removed from this changelog.
+# To read the complete changelog use `apt changelog backone-addon-video`.
diff --git a/debian/.debhelper/generated/backone-addon-video/installed-by-dh_installman b/debian/.debhelper/generated/backone-addon-video/installed-by-dh_installman
new file mode 100644
index 0000000..e69de29
diff --git a/debian/.debhelper/generated/backone-addon-voice/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/backone-addon-voice/dh_installchangelogs.dch.trimmed
new file mode 100644
index 0000000..494c917
--- /dev/null
+++ b/debian/.debhelper/generated/backone-addon-voice/dh_installchangelogs.dch.trimmed
@@ -0,0 +1,257 @@
+backone (1.17.1) stable; urgency=medium
+
+ * Fix /api/backone routes and reinstallable Web UI packages
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 12:02:07 +0700
+
+backone (1.17.0) stable; urgency=medium
+
+ * Tray app, auto web server, peer contact UI
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 09:55:07 +0700
+
+backone (1.16.0) stable; urgency=medium
+
+ * Release notes
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:59:49 +0700
+
+backone (1.15.0) stable; urgency=medium
+
+ * Separated addon architecture: voice, video, chat, updater
+
+ -- Dedy Sutanto Tue, 30 Jun 2026 08:37:16 +0700
+zerotier-one (1.14.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 11 Sep 2024 01:00:00 -0700
+
+zerotier-one (1.14.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 19 Mar 2024 01:00:00 -0700
+
+zerotier-one (1.12.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 12 Sep 2023 01:00:00 -0700
+
+zerotier-one (1.12.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 25 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.12.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 17 Aug 2023 01:00:00 -0700
+
+zerotier-one (1.10.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 21 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 10 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 06 Mar 2023 01:00:00 -0700
+
+zerotier-one (1.10.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Sat, 21 Jan 2023 01:00:00 -0700
+
+zerotier-one (1.10.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Thu, 13 Oct 2022 01:00:00 -0700
+
+zerotier-one (1.10.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 27 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.10.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 11 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.7) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 21 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.6) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 07 Mar 2022 01:00:00 -0700
+
+zerotier-one (1.8.5) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Fri, 17 Dec 2021 01:00:00 -0700
+
+zerotier-one (1.8.4) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 23 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.3) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 15 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.2) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Mon, 08 Nov 2021 01:00:00 -0700
+
+zerotier-one (1.8.1) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 20 Oct 2021 01:00:00 -0700
+
+zerotier-one (1.8.0) unstable; urgency=medium
+
+ * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko Wed, 15 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.6) unstable; urgency=medium
+
+ * Backport endpoint mitigation against address collision attack.
+
+ -- Adam Ierymenko Tue, 21 Sep 2021 01:00:00 -0700
+
+zerotier-one (1.6.5) unstable; urgency=medium
+
+ * Fix path filtering bug that could cause "software laser" effect.
+ * Fix printf overflow in CLI (not exploitable or security related)
+ * Fix Windows device enumeration issue.
+
+ -- Adam Ierymenko Tue, 13 Apr 2021 01:00:00 -0700
+
+zerotier-one (1.6.4) unstable; urgency=medium
+
+ * REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
+
+ -- Adam Ierymenko