const PEER_KEY = 'mobux:peer'; const CRED_PREFIX = 'mobux:peer-cred:'; const MANUAL_KEY = 'mobux:manual-peers';
function getPeer() {
try {
return localStorage.getItem(PEER_KEY) || '';
} catch (_) {
return '';
}
}
function setPeer(peer) {
try {
if (peer) localStorage.setItem(PEER_KEY, peer);
else localStorage.removeItem(PEER_KEY);
} catch (_) {}
}
let pagePeer = null;
function usePeerForPage(peer) {
pagePeer = peer || null;
}
function activePeer() {
return pagePeer != null ? pagePeer : getPeer();
}
function isCurrentNode() {
return !getPeer();
}
function getPeerCred(peer) {
if (!peer) return null;
try {
return localStorage.getItem(CRED_PREFIX + peer) || null;
} catch (_) {
return null;
}
}
function setPeerCred(peer, user, pin) {
if (!peer) return;
try {
localStorage.setItem(CRED_PREFIX + peer, btoa(`${user}:${pin}`));
} catch (_) {}
}
function clearPeerCred(peer) {
if (!peer) return;
try {
localStorage.removeItem(CRED_PREFIX + peer);
} catch (_) {}
}
function normalizeManualPeer(raw) {
const s = (raw || '').trim();
if (!s || s.includes('/') || s.includes(' ')) return null;
const i = s.lastIndexOf(':');
if (i > 0) {
const port = Number(s.slice(i + 1));
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
return `${s.slice(0, i)}:${port}`;
}
const port = location.port || (location.protocol === 'https:' ? '443' : '80');
return `${s}:${port}`;
}
function getManualPeers() {
try {
const arr = JSON.parse(localStorage.getItem(MANUAL_KEY) || '[]');
return Array.isArray(arr) ? arr.filter((p) => typeof p === 'string') : [];
} catch (_) {
return [];
}
}
function addManualPeer(raw) {
const peer = normalizeManualPeer(raw);
if (!peer) return null;
const peers = getManualPeers();
if (!peers.includes(peer)) {
peers.push(peer);
try {
localStorage.setItem(MANUAL_KEY, JSON.stringify(peers));
} catch (_) {}
}
return peer;
}
function removeManualPeer(peer) {
const peers = getManualPeers().filter((p) => p !== peer);
try {
localStorage.setItem(MANUAL_KEY, JSON.stringify(peers));
} catch (_) {}
clearPeerCred(peer);
if (getPeer() === peer) setPeer('');
}
function apiPath(path) {
const p = path.startsWith('/') ? path : `/${path}`;
const peer = activePeer();
if (!peer) return p;
return `/r/${encodeURIComponent(peer)}${p}`;
}
function wsUrl(session) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const peer = activePeer();
if (!peer) {
return `${proto}://${location.host}/ws/${encodeURIComponent(session)}`;
}
let url =
`${proto}://${location.host}/r/${encodeURIComponent(peer)}` +
`/ws/${encodeURIComponent(session)}`;
const cred = getPeerCred(peer);
if (cred) url += `?upstream_auth=${encodeURIComponent(cred)}`;
return url;
}
async function parseError(res) {
let body = null;
try {
body = await res.clone().json();
} catch (_) {}
if (body && body.error) {
return { status: res.status, kind: body.error, message: body.message || body.error };
}
let text = '';
try {
text = await res.clone().text();
} catch (_) {}
return { status: res.status, kind: null, message: text || `${res.status}` };
}
async function trustNewCert(peer) {
const res = await fetch(`/api/peers/${encodeURIComponent(peer)}/pin`, {
method: 'DELETE',
});
if (!res.ok) throw new Error(`failed to reset pin: ${res.status}`);
}
async function apiFetch(path, opts = {}) {
const peer = activePeer();
const url = apiPath(path);
const headers = new Headers(opts.headers || {});
if (peer) {
const cred = getPeerCred(peer);
if (cred) headers.set('X-Mobux-Upstream-Authorization', `Basic ${cred}`);
}
const res = await fetch(url, { ...opts, headers });
if (peer && res.status === 401) {
clearPeerCred(peer);
const err = new Error('peer authentication failed');
err.meshKind = 'unauthorized';
err.peer = peer;
throw err;
}
if (peer && res.status === 409) {
const e = await parseError(res);
if (e.kind === 'pin_mismatch') {
const err = new Error(e.message);
err.meshKind = 'pin_mismatch';
err.peer = peer;
throw err;
}
}
return res;
}
async function apiFetchJSON(path, opts = {}) {
const merged = {
...opts,
headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
};
const res = await apiFetch(path, merged);
if (!res.ok) {
const e = await parseError(res);
throw new Error(e.message);
}
return res.json();
}
window.MobuxMesh = {
getPeer,
setPeer,
usePeerForPage,
activePeer,
isCurrentNode,
getPeerCred,
setPeerCred,
clearPeerCred,
getManualPeers,
addManualPeer,
removeManualPeer,
normalizeManualPeer,
apiPath,
wsUrl,
apiFetch,
apiFetchJSON,
parseError,
trustNewCert,
};