fork: initialize Mycellium UI Private from Mycell-UI@5229e2c
This repo is a hard fork of mycellium-ui dedicated to the mycelium-private experimental track upstream. The two apps coexist on the same machine via distinct app identifiers, polkit actions, and binary names. Renames - package + crate: mycellium-ui → mycellium-ui-private - bundle identifier: tech.threefold.mycellium-ui-private - daemon binary: mycelium-private (separate upstream release tarball) - bootstrap wrapper: /usr/bin/mycellium-bootstrap-private - polkit policy file + action id Functional changes - SidecarConfig.network_name field (UTF-8, 2..=64 bytes) - start() refuses to spawn without a network name AND a 32-byte key file at app_data_dir/network_key.bin; surfaces a clear error rather than letting mycelium-private fail mid-startup - network_key_status / generate / import / export / delete commands; uses OS RNG (rand) and writes 0600 - empty default peers list (no Threefold seed for private overlays) - new Settings → Private network panel: name input, key generate / reveal-hex / import / delete, status indicator Adapted bootstrap script kills both `mycelium` and `mycelium-private` orphans (cross-clash on UDP/9650 + TCP/8990). CI workflow + sidebar branding updated. The README explains the divergence model and how to cherry-pick upstream fixes.
This commit is contained in:
@@ -30,7 +30,7 @@ const navItems = [
|
||||
{ to: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
];
|
||||
|
||||
const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium");
|
||||
const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium Private");
|
||||
|
||||
onMounted(async () => {
|
||||
await config.load();
|
||||
@@ -60,7 +60,7 @@ async function handleStop() {
|
||||
>
|
||||
<aside class="flex w-56 shrink-0 flex-col border-r border-border bg-card">
|
||||
<div class="flex h-14 items-center px-4 border-b border-border">
|
||||
<span class="font-semibold text-base">Mycellium</span>
|
||||
<span class="font-semibold text-base">Mycellium <span class="text-amber-500">Private</span></span>
|
||||
<span
|
||||
class="ml-auto inline-block h-2 w-2 rounded-full"
|
||||
:class="
|
||||
|
||||
@@ -20,6 +20,14 @@ export interface SidecarConfig {
|
||||
peers: string[];
|
||||
tunName: string | null;
|
||||
noTun: boolean;
|
||||
/** UTF-8, 2..=64 bytes. Public — must match across all nodes of the
|
||||
* same private overlay. */
|
||||
networkName: string | null;
|
||||
}
|
||||
|
||||
export interface NetworkKeyStatus {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
// ─── Type-safe invoke wrappers ───────────────────────────────────────────────
|
||||
@@ -167,6 +175,14 @@ export const api = {
|
||||
|
||||
lookupPubkey: (ip: string) => cmd<PubkeyLookup>("lookup_pubkey", { ip }),
|
||||
regenerateIdentity: () => cmd<void>("regenerate_identity"),
|
||||
|
||||
networkKeyStatus: () => cmd<NetworkKeyStatus>("network_key_status"),
|
||||
networkKeyGenerate: (overwrite: boolean) =>
|
||||
cmd<NetworkKeyStatus>("network_key_generate", { overwrite }),
|
||||
networkKeyImport: (hexKey: string, overwrite: boolean) =>
|
||||
cmd<NetworkKeyStatus>("network_key_import", { hexKey, overwrite }),
|
||||
networkKeyExport: () => cmd<string>("network_key_export"),
|
||||
networkKeyDelete: () => cmd<void>("network_key_delete"),
|
||||
};
|
||||
|
||||
/** Format the canonical peer endpoint string the API expects. */
|
||||
|
||||
@@ -6,13 +6,14 @@ import type { SidecarConfig } from "@/lib/api";
|
||||
const STORE_FILE = "config.json";
|
||||
const KEY = "sidecar";
|
||||
|
||||
// A private overlay has no Threefold-operated seed peer. The user must
|
||||
// declare bootstrap peers they trust (their own VPS, known friends…)
|
||||
// before the daemon can usefully start.
|
||||
const DEFAULT_CONFIG: SidecarConfig = {
|
||||
peers: [
|
||||
"tcp://188.40.132.242:9651",
|
||||
"quic://[2a01:4f8:212:fa6::2]:9651",
|
||||
],
|
||||
peers: [],
|
||||
tunName: null,
|
||||
noTun: false,
|
||||
networkName: null,
|
||||
};
|
||||
|
||||
export const useConfigStore = defineStore("config", () => {
|
||||
@@ -33,6 +34,7 @@ export const useConfigStore = defineStore("config", () => {
|
||||
peers: Array.isArray(saved.peers) ? saved.peers : DEFAULT_CONFIG.peers,
|
||||
tunName: saved.tunName ?? null,
|
||||
noTun: !!saved.noTun,
|
||||
networkName: saved.networkName ?? null,
|
||||
};
|
||||
}
|
||||
loaded.value = true;
|
||||
@@ -43,6 +45,7 @@ export const useConfigStore = defineStore("config", () => {
|
||||
peers: next.peers.map((p) => p.trim()).filter(Boolean),
|
||||
tunName: next.tunName?.trim() ? next.tunName.trim() : null,
|
||||
noTun: !!next.noTun,
|
||||
networkName: next.networkName?.trim() ? next.networkName.trim() : null,
|
||||
};
|
||||
const s = await ensureStore();
|
||||
await s.set(KEY, config.value);
|
||||
|
||||
@@ -8,8 +8,13 @@ import {
|
||||
KeyRound,
|
||||
AlertTriangle,
|
||||
TerminalSquare,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Upload,
|
||||
Download,
|
||||
Copy,
|
||||
} from "lucide-vue-next";
|
||||
import { api } from "@/lib/api";
|
||||
import { api, type NetworkKeyStatus } from "@/lib/api";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { useNodeStore } from "@/stores/node";
|
||||
|
||||
@@ -24,6 +29,7 @@ const draft = reactive({
|
||||
peers: "",
|
||||
tunName: "",
|
||||
noTun: false,
|
||||
networkName: "",
|
||||
});
|
||||
const dirty = ref(false);
|
||||
|
||||
@@ -31,6 +37,7 @@ function loadDraft() {
|
||||
draft.peers = config.value.peers.join("\n");
|
||||
draft.tunName = config.value.tunName ?? "";
|
||||
draft.noTun = config.value.noTun;
|
||||
draft.networkName = config.value.networkName ?? "";
|
||||
dirty.value = false;
|
||||
}
|
||||
|
||||
@@ -52,6 +59,7 @@ async function save() {
|
||||
.filter(Boolean),
|
||||
tunName: draft.tunName.trim() || null,
|
||||
noTun: draft.noTun,
|
||||
networkName: draft.networkName.trim() || null,
|
||||
});
|
||||
dirty.value = false;
|
||||
}
|
||||
@@ -101,13 +109,234 @@ async function refreshLogs() {
|
||||
|
||||
const isReady = computed(() => phase.value === "ready");
|
||||
|
||||
// ─── Private network key ────────────────────────────────────────────────────
|
||||
|
||||
const keyStatus = ref<NetworkKeyStatus | null>(null);
|
||||
const keyBusy = ref(false);
|
||||
const keyError = ref<string | null>(null);
|
||||
const importHex = ref("");
|
||||
const exportedHex = ref<string | null>(null);
|
||||
|
||||
async function refreshKeyStatus() {
|
||||
try {
|
||||
keyStatus.value = await api.networkKeyStatus();
|
||||
} catch (e) {
|
||||
keyError.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateKey() {
|
||||
const overwrite = keyStatus.value?.exists ?? false;
|
||||
if (
|
||||
overwrite &&
|
||||
!confirm(
|
||||
"Replace the existing network key? Every node currently using it will be cut off until they're given the new one.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
keyBusy.value = true;
|
||||
keyError.value = null;
|
||||
exportedHex.value = null;
|
||||
try {
|
||||
keyStatus.value = await api.networkKeyGenerate(overwrite);
|
||||
} catch (e) {
|
||||
keyError.value = String(e);
|
||||
} finally {
|
||||
keyBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function importKey() {
|
||||
const hex = importHex.value.trim();
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
||||
keyError.value = "Network key must be exactly 64 hex characters (32 bytes).";
|
||||
return;
|
||||
}
|
||||
const overwrite = keyStatus.value?.exists ?? false;
|
||||
if (
|
||||
overwrite &&
|
||||
!confirm("Replace the existing network key with the one you pasted?")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
keyBusy.value = true;
|
||||
keyError.value = null;
|
||||
try {
|
||||
keyStatus.value = await api.networkKeyImport(hex, overwrite);
|
||||
importHex.value = "";
|
||||
} catch (e) {
|
||||
keyError.value = String(e);
|
||||
} finally {
|
||||
keyBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportKey() {
|
||||
keyError.value = null;
|
||||
try {
|
||||
exportedHex.value = await api.networkKeyExport();
|
||||
} catch (e) {
|
||||
keyError.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyExported() {
|
||||
if (!exportedHex.value) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(exportedHex.value);
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKey() {
|
||||
if (
|
||||
!confirm(
|
||||
"Delete the network key? You won't be able to start the daemon until you generate or import a new one.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
keyBusy.value = true;
|
||||
keyError.value = null;
|
||||
try {
|
||||
await api.networkKeyDelete();
|
||||
exportedHex.value = null;
|
||||
await refreshKeyStatus();
|
||||
} catch (e) {
|
||||
keyError.value = String(e);
|
||||
} finally {
|
||||
keyBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshKeyStatus();
|
||||
if (isReady.value) await refreshLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<!-- Private network ────────────────────────────────────────────────── -->
|
||||
<section class="rounded-lg border border-amber-500/40 bg-amber-500/5 lg:col-span-2">
|
||||
<header class="border-b border-amber-500/30 px-4 py-3">
|
||||
<h2 class="flex items-center gap-2 text-sm font-medium">
|
||||
<ShieldCheck class="h-4 w-4 text-amber-500" /> Private network
|
||||
</h2>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
A private overlay is identified by a <strong>name</strong> (public, agreed
|
||||
across nodes) and a 32-byte <strong>shared key</strong> (secret, distributed
|
||||
out-of-band). Both must match exactly across every node that
|
||||
should be on the same overlay.
|
||||
</p>
|
||||
</header>
|
||||
<div class="space-y-4 px-4 py-4">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
Network name (UTF-8, 2–64 bytes — public)
|
||||
</label>
|
||||
<input
|
||||
v-model="draft.networkName"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
placeholder="acme-corp-private"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-1.5 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
Network key file
|
||||
</label>
|
||||
<span
|
||||
class="text-xs"
|
||||
:class="keyStatus?.exists ? 'text-emerald-500' : 'text-destructive'"
|
||||
>
|
||||
{{ keyStatus?.exists ? "configured" : "missing" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 break-all rounded-md border border-border bg-muted/40 p-2 font-mono text-[11px]">
|
||||
{{ keyStatus?.path ?? "(unknown)" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-amber-500 px-3 py-1.5 text-xs font-medium text-amber-950 hover:opacity-90 disabled:opacity-50"
|
||||
:disabled="keyBusy"
|
||||
@click="generateKey"
|
||||
>
|
||||
<Sparkles class="h-3 w-3" />
|
||||
{{ keyStatus?.exists ? "Re-generate" : "Generate 32-byte key" }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-secondary disabled:opacity-50"
|
||||
:disabled="keyBusy || !keyStatus?.exists"
|
||||
@click="exportKey"
|
||||
>
|
||||
<Download class="h-3 w-3" />
|
||||
Reveal hex
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-destructive px-3 py-1.5 text-xs text-destructive hover:bg-destructive hover:text-destructive-foreground disabled:opacity-50"
|
||||
:disabled="keyBusy || !keyStatus?.exists"
|
||||
@click="deleteKey"
|
||||
>
|
||||
<RotateCcw class="h-3 w-3" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="exportedHex" class="space-y-2">
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
Hex export — share over a secure channel only
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
:value="exportedHex"
|
||||
readonly
|
||||
class="flex-1 rounded-md border border-input bg-background px-3 py-1.5 font-mono text-xs focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-secondary"
|
||||
@click="copyExported"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
Import a key from another node (64 hex characters)
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="importHex"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
placeholder="0a1b2c3d…"
|
||||
class="flex-1 rounded-md border border-input bg-background px-3 py-1.5 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
|
||||
:disabled="keyBusy || !importHex.trim()"
|
||||
@click="importKey"
|
||||
>
|
||||
<Upload class="h-3 w-3" />
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="keyError" class="text-xs text-destructive">{{ keyError }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Daemon configuration ───────────────────────────────────────────── -->
|
||||
<section class="rounded-lg border border-border bg-card">
|
||||
<header class="border-b border-border px-4 py-3">
|
||||
@@ -120,15 +349,20 @@ onMounted(async () => {
|
||||
<div class="space-y-4 px-4 py-4">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
Static peers (one per line)
|
||||
Bootstrap peers (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
v-model="draft.peers"
|
||||
rows="6"
|
||||
spellcheck="false"
|
||||
class="mt-1 w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="tcp://188.40.132.242:9651"
|
||||
placeholder="tcp://your-node.example.org:9651"
|
||||
/>
|
||||
<p class="mt-1 text-[11px] text-muted-foreground">
|
||||
Private overlays don't have a Threefold-operated seed — point at
|
||||
your own VPS or other trusted nodes that already share the network
|
||||
name and key.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user