@@ -119,7 +236,7 @@
@@ -154,13 +271,43 @@ const DEFAULT_DOWNLOAD_URL = IS_DEMO
const PAIR_SCHEME = 'archipelago://pair'
const DEMO_SERVER_URL = 'https://demo.archipelago-foundation.org'
+// Remote-access onboarding: inserts the WireGuard install + tunnel-QR steps
+// between "I've installed it" and the pairing QR. Real nodes pair against the
+// node's VPN address instead of the LAN origin; the demo walks the same steps
+// with a mock tunnel config (its pairing QR still carries the public demo
+// URL). Set to false to restore the original two-screen flow (instant revert).
+const REMOTE_ACCESS_STEPS = true
+
+// The WireGuard peer config only routes the VPN subnet (AllowedIPs
+// 10.44.0.0/16 — core/archipelago/src/api/rpc/vpn.rs), so away from the LAN
+// the node is reachable exclusively at its tunnel address. The pairing QR
+// must therefore advertise this, not the browser's LAN origin. The flow has
+// the user switch the tunnel on right before pairing, so it validates.
+const VPN_NODE_URL = 'http://10.44.0.1'
+
+// Official WireGuard Android app (com.wireguard.android 1.0.20260315,
+// sha256 f92971bc…, mirrored from download.wireguard.com), served like the
+// companion APK: gitea raw-on-main for nodes, own origin for the demo.
+const WIREGUARD_DOWNLOAD_URL = IS_DEMO
+ ? `${window.location.origin}/packages/wireguard.apk`
+ : 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/wireguard.apk'
+
+// Name of the auto-created VPN peer for the phone running the companion.
+const WG_PEER_NAME = 'companion-phone'
+
const visible = ref(false)
-const step = ref<'download' | 'pair'>('download')
+const step = ref<'download' | 'wireguard' | 'wgqr' | 'pair'>('download')
const slideName = ref('slide-forward')
const qrDataUrl = ref('')
const pairQrDataUrl = ref('')
const pairingUrl = ref('')
+const wgApkQrDataUrl = ref('')
+const wgQrDataUrl = ref('')
+const wgConfig = ref('')
+const wgLoading = ref(false)
+const wgError = ref('')
const companionDownloadUrl = import.meta.env.VITE_COMPANION_APK_URL || DEFAULT_DOWNLOAD_URL
+const wireguardDownloadUrl = import.meta.env.VITE_WIREGUARD_APK_URL || WIREGUARD_DOWNLOAD_URL
const loginTransition = useLoginTransitionStore()
@@ -232,6 +379,17 @@ watch(visible, async (isVisible) => {
light: '#ffffff',
},
})
+ if (REMOTE_ACCESS_STEPS && !wgApkQrDataUrl.value) {
+ wgApkQrDataUrl.value = await QRCode.toDataURL(wireguardDownloadUrl, {
+ width: 512,
+ margin: 2,
+ errorCorrectionLevel: 'M',
+ color: {
+ dark: '#111111',
+ light: '#ffffff',
+ },
+ })
+ }
}, { immediate: true, flush: 'post' })
/**
@@ -242,6 +400,9 @@ watch(visible, async (isVisible) => {
*/
async function resolveServerUrl(): Promise
{
if (IS_DEMO) return DEMO_SERVER_URL
+ // Remote-access flow: the app must connect through the tunnel the user just
+ // switched on — the LAN origin is unreachable from outside AllowedIPs.
+ if (REMOTE_ACCESS_STEPS) return VPN_NODE_URL
const { hostname, origin } = window.location
if (hostname !== 'localhost' && hostname !== '127.0.0.1') return origin
try {
@@ -285,6 +446,77 @@ function showDownloadScreen() {
step.value = 'download'
}
+function advanceFromDownload() {
+ if (REMOTE_ACCESS_STEPS) showWireguardScreen('forward')
+ else void showPairScreen()
+}
+
+function showWireguardScreen(direction: 'forward' | 'back' = 'forward') {
+ slideName.value = direction === 'forward' ? 'slide-forward' : 'slide-back'
+ step.value = 'wireguard'
+}
+
+function showWgQrScreen() {
+ slideName.value = 'slide-forward'
+ step.value = 'wgqr'
+ void loadWgPeer()
+}
+
+function backFromPair() {
+ if (REMOTE_ACCESS_STEPS) {
+ slideName.value = 'slide-back'
+ step.value = 'wgqr'
+ } else {
+ showDownloadScreen()
+ }
+}
+
+// Create (or fetch) the phone's VPN peer and render its config as a QR.
+// Reuses the same RPCs as the Server page's Add Device modal; the peer is
+// looked up first so reopening the modal never duplicates it.
+async function loadWgPeer() {
+ if (wgQrDataUrl.value || wgLoading.value) return
+ wgLoading.value = true
+ wgError.value = ''
+ try {
+ const listed = await rpcClient
+ .call<{ peers: { name: string }[] }>({ method: 'vpn.list-peers' })
+ .catch(() => ({ peers: [] as { name: string }[] }))
+ const exists = (listed.peers || []).some((p) => p.name === WG_PEER_NAME)
+ const res = await rpcClient.call<{ config: string; peer_ip: string }>({
+ method: exists ? 'vpn.peer-config' : 'vpn.create-peer',
+ params: { name: WG_PEER_NAME },
+ })
+ wgConfig.value = res.config
+ wgQrDataUrl.value = await QRCode.toDataURL(res.config, {
+ width: 512,
+ margin: 3,
+ errorCorrectionLevel: 'M',
+ color: {
+ dark: '#111111',
+ light: '#ffffff',
+ },
+ })
+ } catch (e) {
+ wgError.value = e instanceof Error ? e.message : 'Failed to generate the tunnel config'
+ } finally {
+ wgLoading.value = false
+ }
+}
+
+// Same-device path: hand the config to the WireGuard app as an importable
+// .conf file (a phone can't scan its own screen).
+function downloadWgConfig() {
+ if (!wgConfig.value) return
+ const blob = new Blob([wgConfig.value], { type: 'application/octet-stream' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = 'archipelago.conf'
+ a.click()
+ URL.revokeObjectURL(url)
+}
+
function dismiss() {
visible.value = false
step.value = 'download'
diff --git a/neode-ui/src/views/web5/Web5ConnectedNodes.vue b/neode-ui/src/views/web5/Web5ConnectedNodes.vue
index b376a3e0..9d4db73d 100644
--- a/neode-ui/src/views/web5/Web5ConnectedNodes.vue
+++ b/neode-ui/src/views/web5/Web5ConnectedNodes.vue
@@ -11,21 +11,6 @@
{{ t('web5.connectedNodes') }}
-
-
-
-