Initial Vynte Connect clients

This commit is contained in:
2026-05-26 16:41:14 -05:00
commit 980535d682
35 changed files with 4958 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
# Build outputs and dependency folders
apps/vynte-connect/.build/
apps/vynte-connect/dist/
apps/vynte-connect-windows/node_modules/
apps/vynte-connect-windows/dist/
# Locally fetched NetBird runtime assets
apps/vynte-connect/Resources/netbird
apps/vynte-connect-windows/resources/netbird.exe
apps/vynte-connect-windows/resources/wintun.dll
.tmp-netbird-assets/
+54
View File
@@ -0,0 +1,54 @@
# Vynte Connect
Vynte Connect is the branded client wrapper for the Vynte NetBird VPN at `https://vpn.vyntehome.com`.
This source tree contains:
- `apps/vynte-connect`: macOS 14 SwiftUI menu bar app.
- `apps/vynte-connect-windows`: Windows Electron tray app.
- `docs/vynte-connect-design-spec.md`: product and UI behavior notes.
- `scripts/fetch-netbird-assets.mjs`: fetches the official NetBird runtime assets needed for packaging.
The apps do not implement VPN networking themselves. They bundle and drive the official NetBird CLI, install/start the local NetBird helper service when needed, then poll `netbird status --json` until the tunnel is ready.
## Prepare NetBird Assets
Downloaded binaries are intentionally ignored by git. Fetch them before packaging:
```bash
node scripts/fetch-netbird-assets.mjs
```
To pin a version:
```bash
NETBIRD_VERSION=v0.71.4 node scripts/fetch-netbird-assets.mjs
```
## macOS
```bash
cd apps/vynte-connect
swift build -c release
./Scripts/package-zip.sh
```
Output:
- `apps/vynte-connect/dist/Vynte Connect.app`
- `apps/vynte-connect/dist/Vynte-Connect-macOS.zip`
## Windows
```bash
cd apps/vynte-connect-windows
npm install
npm run package:win
```
Output:
- `apps/vynte-connect-windows/dist/Vynte Connect-win32-x64/`
- `apps/vynte-connect-windows/dist/Vynte-Connect-Windows-x64.zip`
The Windows package is a portable x64 app bundle. It is not code-signed and does not stamp Windows executable metadata, so it can be built from macOS without Wine.
+41
View File
@@ -0,0 +1,41 @@
# Vynte Connect for Windows
Vynte Connect for Windows is a tray wrapper around the bundled `netbird.exe` CLI for `https://vpn.vyntehome.com`.
It exposes the same basic flow as the macOS app:
- Connect to `vpn.vyntehome.com`
- Disconnect the tunnel
- Log out so the next connect registers with NetBird again
- Show gateway, internal IP, peer count, and connection state
## Build
Place the Windows NetBird runtime files in `resources/` before packaging:
- `resources/netbird.exe`
- `resources/wintun.dll`
```powershell
npm install
npm run package:win
```
The packaged app is written to `dist/Vynte Connect-win32-x64/`, and a portable zip is written to `dist/Vynte-Connect-Windows-x64.zip`.
This build intentionally creates a portable Windows app without stamping Windows executable metadata, so it can be built from macOS without Wine.
## Runtime behavior
On first connect, the app uses Windows UAC to install/start the bundled Vynte network helper service:
```powershell
netbird.exe service install --management-url https://vpn.vyntehome.com --admin-url https://vpn.vyntehome.com
netbird.exe service start
```
Then it runs:
```powershell
netbird.exe up --management-url https://vpn.vyntehome.com --admin-url https://vpn.vyntehome.com
```
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "vynte-connect-windows",
"version": "0.1.0",
"private": true,
"description": "Vynte Connect Windows tray wrapper for vpn.vyntehome.com",
"main": "src/main.js",
"scripts": {
"start": "electron .",
"package:win": "node scripts/package-win-portable.mjs"
},
"devDependencies": {
"electron": "^36.3.1",
"electron-packager": "^17.1.2"
}
}
@@ -0,0 +1,16 @@
This BSD3Clause license applies to all parts of the repository except for the directories management/, signal/, relay/ and combined/.
Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory.
BSD 3-Clause License
Copyright (c) 2022 NetBird GmbH & AUTHORS
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

@@ -0,0 +1,64 @@
import { existsSync, mkdirSync, rmSync, copyFileSync, cpSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import { spawnSync } from "node:child_process";
const root = join(import.meta.dirname, "..");
const electronVersion = "36.9.5";
const cacheRoot = join(homedir(), "Library", "Caches", "electron");
const zipName = `electron-v${electronVersion}-win32-x64.zip`;
const appDir = join(root, "dist", "Vynte Connect-win32-x64");
const zipPath = join(root, "dist", "Vynte-Connect-Windows-x64.zip");
function run(command, args, options = {}) {
const result = spawnSync(command, args, { stdio: "inherit", ...options });
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}`);
}
}
function findElectronZip(dir) {
if (!existsSync(dir)) return null;
const result = spawnSync("find", [dir, "-name", zipName, "-print", "-quit"], { encoding: "utf8" });
return result.stdout.trim() || null;
}
function sha256(file) {
const result = spawnSync("shasum", ["-a", "256", file], { encoding: "utf8" });
if (result.status !== 0) return "unknown";
return result.stdout.split(/\s+/)[0];
}
const electronZip = findElectronZip(cacheRoot);
if (!electronZip) {
throw new Error(`Missing ${zipName}. Run npm install once so Electron downloads its Windows runtime.`);
}
mkdirSync(join(root, "dist"), { recursive: true });
rmSync(appDir, { recursive: true, force: true });
rmSync(zipPath, { force: true });
mkdirSync(appDir, { recursive: true });
run("ditto", ["-x", "-k", electronZip, appDir]);
renameSync(join(appDir, "electron.exe"), join(appDir, "Vynte Connect.exe"));
const packagedAppDir = join(appDir, "resources", "app");
mkdirSync(packagedAppDir, { recursive: true });
copyFileSync(join(root, "package.json"), join(packagedAppDir, "package.json"));
cpSync(join(root, "src"), join(packagedAppDir, "src"), { recursive: true });
cpSync(join(root, "resources"), join(packagedAppDir, "resources"), { recursive: true });
writeFileSync(
join(appDir, "BUILD-INFO.txt"),
[
"Vynte Connect for Windows",
`Electron runtime: ${basename(electronZip)}`,
`Electron runtime sha256: ${sha256(electronZip)}`,
"Gateway: https://vpn.vyntehome.com",
"",
].join("\n"),
);
run("zip", ["-qry", zipPath, "."], { cwd: appDir });
console.log(`Packaged ${appDir}`);
console.log(`Created ${zipPath}`);
+19
View File
@@ -0,0 +1,19 @@
const MANAGEMENT_URL = 'https://vpn.vyntehome.com';
const GATEWAY_HOST = new URL(MANAGEMENT_URL).host;
module.exports = {
APP_NAME: 'Vynte Connect',
MANAGEMENT_URL,
GATEWAY_HOST,
POLL_INTERVAL_MS: 5000,
DEFAULT_STATUS: {
state: 'checking',
title: `Checking ${GATEWAY_HOST}`,
message: 'Preparing the Vynte access client.',
ip: '',
fqdn: '',
peers: '0/0',
connectedSince: null,
gatewayHost: GATEWAY_HOST
}
};
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Vynte Connect</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<main class="panel">
<header class="header">
<img class="logo-small" src="../resources/VynteIcon.png" alt="">
<div class="title-block">
<div class="title">Vynte Connect</div>
<div id="identity" class="identity">Vynte access</div>
</div>
<button id="refresh" class="icon-button" title="Refresh"></button>
</header>
<section class="hero">
<button id="orb" class="orb" aria-label="Connect or disconnect">
<span class="orb-ring"></span>
<span class="orb-core"></span>
<span class="orb-dot"></span>
<img id="orb-logo" class="orb-logo" src="../resources/VynteIcon.png" alt="">
<span id="orb-power" class="orb-power"></span>
</button>
<div class="status-line">
<span id="status-dot" class="status-dot"></span>
<span id="status-title">Checking vpn.vyntehome.com</span>
</div>
<div id="status-message" class="message">Preparing the Vynte access client.</div>
</section>
<section class="gateway">
<div class="server-icon"></div>
<div>
<div class="label">Gateway</div>
<div class="mono" data-gateway-host>vpn.vyntehome.com</div>
</div>
<span id="gateway-state" class="badge">Offline</span>
</section>
<section id="stats" class="stats hidden">
<div class="stat">
<div class="label">Internal IP</div>
<div id="ip" class="mono">Assigned</div>
</div>
<div class="divider"></div>
<div class="stat">
<div class="label">Peers</div>
<div id="peers" class="mono">0/0</div>
</div>
</section>
<footer class="footer">
<button id="auto" class="chip">
<span></span>
<span>Auto-connect</span>
<span id="toggle" class="toggle"><span></span></span>
</button>
<span class="spacer"></span>
<button id="logout" class="icon-button hidden" title="Log out"></button>
<button id="quit" class="icon-button" title="Quit"></button>
</footer>
</main>
<script src="./renderer.js"></script>
</body>
</html>
+175
View File
@@ -0,0 +1,175 @@
const { app, BrowserWindow, Tray, ipcMain, nativeImage, shell } = require('electron');
const path = require('path');
const { APP_NAME, DEFAULT_STATUS, GATEWAY_HOST, MANAGEMENT_URL, POLL_INTERVAL_MS } = require('./config');
const { NetBirdClient } = require('./netbird');
const { baseStatus, normalizeStatus } = require('./status');
let tray;
let window;
let pollTimer;
let autoConnect = false;
let cachedStatus = { ...DEFAULT_STATUS };
function resourcePath(name) {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'app', 'resources', name);
}
return path.join(app.getAppPath(), 'resources', name);
}
function netbirdClient() {
return new NetBirdClient(resourcePath('netbird.exe'));
}
function withAutoConnect(status) {
return { ...status, autoConnect };
}
function broadcastStatus() {
if (window && !window.isDestroyed()) {
window.webContents.send('status', withAutoConnect(cachedStatus));
}
}
function updateTray() {
if (!tray) return;
const label = cachedStatus.state === 'connected' ? `${APP_NAME}: Connected` : APP_NAME;
tray.setToolTip(label);
}
function setStatus(status) {
cachedStatus = {
...cachedStatus,
...status,
gatewayHost: GATEWAY_HOST
};
broadcastStatus();
updateTray();
return cachedStatus;
}
async function refreshStatus() {
const result = await netbirdClient().status();
if (!result.ok) {
return setStatus(baseStatus({
state: 'disconnected',
title: 'Disconnected',
message: 'Click the flame to install the Vynte network helper.'
}));
}
return setStatus(normalizeStatus(result.stdout, cachedStatus));
}
async function connect() {
setStatus({
state: 'connecting',
title: 'Connecting...',
message: 'Starting secure Vynte access...'
});
const client = netbirdClient();
const serviceReady = await client.ensureService();
if (!serviceReady) {
return setStatus({
state: 'error',
title: 'Connection failed',
message: `${GATEWAY_HOST} helper did not become ready.`
});
}
const result = await client.connect();
if (!result.ok) {
const output = `${result.stdout}\n${result.stderr}`.trim();
return setStatus({
state: 'error',
title: 'Connection failed',
message: output || `${GATEWAY_HOST} command failed.`
});
}
return refreshStatus();
}
async function disconnect() {
setStatus({
state: 'disconnecting',
title: 'Disconnecting...',
message: 'Closing secure Vynte access...'
});
await netbirdClient().disconnect();
return refreshStatus();
}
async function logout() {
setStatus({
state: 'loggingOut',
title: 'Logging out',
message: "Removing this PC's NetBird registration."
});
await netbirdClient().logout();
return refreshStatus();
}
function createWindow() {
window = new BrowserWindow({
width: 384,
height: 456,
show: false,
resizable: false,
frame: false,
alwaysOnTop: true,
skipTaskbar: true,
backgroundColor: '#191b20',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
window.loadFile(path.join(__dirname, 'index.html'));
window.on('blur', () => window.hide());
}
function showWindow() {
const bounds = tray.getBounds();
const windowBounds = window.getBounds();
const x = Math.round(bounds.x + bounds.width - windowBounds.width);
const y = Math.round(bounds.y - windowBounds.height - 8);
window.setPosition(Math.max(x, 0), Math.max(y, 0), false);
window.show();
window.focus();
broadcastStatus();
}
function createTray() {
const icon = nativeImage.createFromPath(resourcePath('VynteIcon.png')).resize({ width: 18, height: 18 });
tray = new Tray(icon);
tray.setToolTip(APP_NAME);
tray.on('click', showWindow);
}
app.whenReady().then(() => {
createWindow();
createTray();
refreshStatus();
pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS);
});
app.on('before-quit', () => {
if (pollTimer) clearInterval(pollTimer);
});
ipcMain.handle('status:get', () => withAutoConnect(cachedStatus));
ipcMain.handle('status:refresh', refreshStatus);
ipcMain.handle('vpn:connect', connect);
ipcMain.handle('vpn:disconnect', disconnect);
ipcMain.handle('vpn:logout', logout);
ipcMain.handle('auto:toggle', () => {
autoConnect = !autoConnect;
broadcastStatus();
return autoConnect;
});
ipcMain.handle('app:quit', () => app.quit());
ipcMain.handle('external:openGateway', () => shell.openExternal(MANAGEMENT_URL));
+100
View File
@@ -0,0 +1,100 @@
const { execFile } = require('child_process');
const { MANAGEMENT_URL } = require('./config');
class NetBirdClient {
constructor(executablePath) {
this.executablePath = executablePath;
}
run(args, options = {}) {
return new Promise((resolve) => {
execFile(this.executablePath, args, {
windowsHide: true,
timeout: options.timeout || 30000
}, (error, stdout, stderr) => {
resolve({
ok: !error,
code: error && typeof error.code === 'number' ? error.code : 0,
stdout: stdout || '',
stderr: stderr || '',
error
});
});
});
}
runElevated(args) {
const quotedExe = this.executablePath.replace(/'/g, "''");
const quotedArgs = args.map((arg) => `'${String(arg).replace(/'/g, "''")}'`).join(',');
const script = `Start-Process -FilePath '${quotedExe}' -ArgumentList @(${quotedArgs}) -Verb RunAs -Wait`;
return new Promise((resolve) => {
execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
windowsHide: true,
timeout: 180000
}, (error, stdout, stderr) => {
resolve({
ok: !error,
stdout: stdout || '',
stderr: stderr || '',
error
});
});
});
}
status(options = {}) {
return this.run(['status', '--json'], { timeout: options.timeout || 12000 });
}
async ensureService() {
const status = await this.status({ timeout: 10000 });
if (status.ok) {
return true;
}
await this.runElevated([
'service',
'install',
'--management-url', MANAGEMENT_URL,
'--admin-url', MANAGEMENT_URL,
'--log-level', 'info'
]);
await this.runElevated(['service', 'start']);
for (let index = 0; index < 20; index += 1) {
const check = await this.status({ timeout: 8000 });
if (check.ok) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
connect() {
return this.run([
'up',
'--management-url', MANAGEMENT_URL,
'--admin-url', MANAGEMENT_URL
], { timeout: 180000 });
}
disconnect() {
return this.run(['down', '--management-url', MANAGEMENT_URL], { timeout: 45000 });
}
async logout() {
await this.run(['down', '--management-url', MANAGEMENT_URL], { timeout: 30000 });
return this.run([
'deregister',
'--management-url', MANAGEMENT_URL,
'--admin-url', MANAGEMENT_URL
], { timeout: 60000 });
}
}
module.exports = {
NetBirdClient
};
+13
View File
@@ -0,0 +1,13 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('vynte', {
getStatus: () => ipcRenderer.invoke('status:get'),
refresh: () => ipcRenderer.invoke('status:refresh'),
connect: () => ipcRenderer.invoke('vpn:connect'),
disconnect: () => ipcRenderer.invoke('vpn:disconnect'),
logout: () => ipcRenderer.invoke('vpn:logout'),
toggleAuto: () => ipcRenderer.invoke('auto:toggle'),
quit: () => ipcRenderer.invoke('app:quit'),
openGateway: () => ipcRenderer.invoke('external:openGateway'),
onStatus: (callback) => ipcRenderer.on('status', (_event, status) => callback(status))
});
@@ -0,0 +1,46 @@
const body = document.body;
const orb = document.getElementById('orb');
const statusTitle = document.getElementById('status-title');
const statusMessage = document.getElementById('status-message');
const identity = document.getElementById('identity');
const gatewayState = document.getElementById('gateway-state');
const stats = document.getElementById('stats');
const ip = document.getElementById('ip');
const peers = document.getElementById('peers');
const refresh = document.getElementById('refresh');
const auto = document.getElementById('auto');
const logout = document.getElementById('logout');
const quit = document.getElementById('quit');
let currentStatus = { state: 'checking' };
function setStatus(status) {
currentStatus = status;
body.className = status.state || 'disconnected';
statusTitle.textContent = status.title || 'Disconnected';
statusMessage.textContent = status.message || '';
identity.textContent = status.fqdn || 'Vynte access';
document.querySelector('[data-gateway-host]').textContent = status.gatewayHost || 'vpn.vyntehome.com';
gatewayState.textContent = status.state === 'connected' ? 'Live' : 'Offline';
ip.textContent = status.ip || 'Assigned';
peers.textContent = status.peers || '0/0';
stats.classList.toggle('hidden', status.state !== 'connected');
logout.classList.toggle('hidden', status.state !== 'connected');
auto.classList.toggle('active', Boolean(status.autoConnect));
}
orb.addEventListener('click', async () => {
if (currentStatus.state === 'connected') {
await window.vynte.disconnect();
} else if (!['connecting', 'disconnecting', 'loggingOut', 'checking'].includes(currentStatus.state)) {
await window.vynte.connect();
}
});
refresh.addEventListener('click', () => window.vynte.refresh());
auto.addEventListener('click', () => window.vynte.toggleAuto());
logout.addEventListener('click', () => window.vynte.logout());
quit.addEventListener('click', () => window.vynte.quit());
window.vynte.onStatus(setStatus);
window.vynte.getStatus().then(setStatus);
+57
View File
@@ -0,0 +1,57 @@
const { GATEWAY_HOST } = require('./config');
function baseStatus(overrides) {
return {
ip: '',
fqdn: '',
peers: '0/0',
connectedSince: null,
gatewayHost: GATEWAY_HOST,
...overrides
};
}
function normalizeStatus(raw, previousStatus = {}) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return baseStatus({
state: 'error',
title: 'Connection failed',
message: `${GATEWAY_HOST} status could not be parsed.`
});
}
const daemonConnected = String(parsed.daemonStatus || '').toLowerCase().includes('connected');
const managementConnected = parsed.management && parsed.management.connected === true;
const signalConnected = parsed.signal && parsed.signal.connected === true;
const hasIdentity = Boolean(parsed.netbirdIp || parsed.fqdn);
const connected = daemonConnected && (managementConnected || signalConnected || hasIdentity);
const peerCount = parsed.peers || {};
if (connected) {
return baseStatus({
state: 'connected',
title: 'Connected',
message: "You're on the Vynte network.",
ip: parsed.netbirdIp || 'Assigned',
fqdn: parsed.fqdn || '',
peers: `${peerCount.connected || 0}/${peerCount.total || 0}`,
connectedSince: previousStatus.connectedSince || Date.now()
});
}
return baseStatus({
state: 'disconnected',
title: 'Disconnected',
message: 'Click the flame to connect with NetBird.',
fqdn: parsed.fqdn || '',
peers: `${peerCount.connected || 0}/${peerCount.total || 0}`
});
}
module.exports = {
baseStatus,
normalizeStatus
};
+273
View File
@@ -0,0 +1,273 @@
:root {
--bg: #191b20;
--panel: rgba(20, 22, 26, .94);
--line: rgba(255, 255, 255, .07);
--text: #f5f6f8;
--muted: #9099a3;
--low: #5a5d66;
--orange: #ff7a1a;
--pink: #e63e5c;
--purple: #6f2bae;
--blue: #1e90ff;
--gradient: linear-gradient(135deg, var(--orange), var(--pink) 38%, var(--purple) 70%, var(--blue));
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }
body {
color: var(--text);
font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
background:
radial-gradient(120% 80% at 20% 0%, rgba(255, 122, 26, .18), transparent 55%),
radial-gradient(90% 80% at 100% 100%, rgba(30, 144, 255, .16), transparent 60%),
var(--bg);
}
.panel {
width: 100%;
min-height: 100%;
background: rgba(25, 27, 32, .88);
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
}
.header, .footer {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}
.footer {
border-top: 1px solid var(--line);
border-bottom: 0;
padding: 8px 6px;
}
.logo-small { width: 22px; height: 22px; border-radius: 4px; }
.title-block { flex: 1; min-width: 0; }
.title { font-size: 13px; font-weight: 650; }
.identity {
color: var(--muted);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
button {
font-family: inherit;
color: inherit;
}
.icon-button {
width: 30px;
height: 26px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
cursor: pointer;
}
.icon-button:hover { background: rgba(255,255,255,.08); color: var(--text); }
.hero {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 16px 16px;
}
.orb {
width: 126px;
height: 126px;
border: 0;
padding: 0;
position: relative;
background: transparent;
cursor: pointer;
}
.orb-ring {
position: absolute;
inset: 5px;
border-radius: 50%;
background: conic-gradient(from 90deg, #2a2d33, #3a3d44, #2a2d33);
transition: background .25s, filter .25s;
}
.orb-core {
position: absolute;
inset: 7px;
border-radius: 50%;
background: radial-gradient(circle at 30% 25%, #1f2228 0%, #14161a 70%);
}
.orb-logo, .orb-power {
position: absolute;
inset: 0;
margin: auto;
}
.orb-logo {
width: 52px;
height: 52px;
display: none;
border-radius: 10px;
}
.orb-power {
width: 52px;
height: 52px;
display: grid;
place-items: center;
font-size: 42px;
color: #7a7e87;
}
.orb-dot {
display: none;
position: absolute;
left: 59px;
top: 1px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 14px rgba(255, 122, 26, .78);
}
.connected .orb-ring,
.connecting .orb-ring,
.disconnecting .orb-ring,
.loggingOut .orb-ring {
background: conic-gradient(from 0deg, var(--orange), var(--pink), var(--purple), var(--blue), var(--orange));
filter: drop-shadow(0 0 20px rgba(255,122,26,.28));
}
.connected .orb-logo { display: block; }
.connected .orb-power { display: none; }
.connecting .orb-ring { animation: spin 1.05s linear infinite, pulse 1s ease-in-out infinite; }
.disconnecting .orb-ring, .loggingOut .orb-ring { animation: spin-reverse 1.35s linear infinite, pulse 1s ease-in-out infinite; }
.connecting .orb-dot, .disconnecting .orb-dot, .loggingOut .orb-dot { display: block; animation: orbit 1.05s linear infinite; transform-origin: 4px 62px; }
.disconnecting .orb-dot, .loggingOut .orb-dot { background: var(--purple); animation-direction: reverse; }
.status-line {
margin-top: 8px;
font-size: 16px;
font-weight: 650;
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
display: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 0 4px rgba(255,122,26,.14), 0 0 14px rgba(255,122,26,.7);
}
.connected .status-dot, .error .status-dot { display: inline-block; }
.error .status-dot { background: var(--pink); box-shadow: none; }
.message {
color: var(--muted);
font-size: 12px;
line-height: 1.35;
margin-top: 4px;
text-align: center;
min-height: 32px;
}
.gateway, .stats {
margin: 0 14px 12px;
padding: 10px;
border: 1px solid var(--line);
background: rgba(255,255,255,.04);
border-radius: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.server-icon {
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(255,255,255,.04);
color: var(--muted);
display: grid;
place-items: center;
}
.label {
color: var(--low);
text-transform: uppercase;
font-size: 10px;
letter-spacing: .6px;
}
.mono {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: 12.5px;
}
.badge {
margin-left: auto;
padding: 3px 7px;
border-radius: 999px;
text-transform: uppercase;
font-size: 10px;
font-weight: 700;
color: var(--low);
background: rgba(255,255,255,.06);
}
.connected .badge {
color: #ff9747;
background: rgba(255,122,26,.14);
}
.stats { padding: 0; gap: 0; overflow: hidden; }
.stat {
flex: 1;
padding: 9px 12px;
background: rgba(0,0,0,.16);
}
.divider { width: 1px; align-self: stretch; background: var(--line); }
.hidden { display: none !important; }
.chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
border: 0;
background: transparent;
color: var(--muted);
font-size: 11px;
cursor: pointer;
}
.chip.active {
background: rgba(255,122,26,.10);
color: #ff9747;
}
.toggle {
width: 22px;
height: 12px;
border-radius: 99px;
background: rgba(255,255,255,.10);
position: relative;
}
.toggle span {
position: absolute;
top: 1.5px;
left: 1.5px;
width: 9px;
height: 9px;
border-radius: 50%;
background: white;
transition: left .18s;
}
.chip.active .toggle {
background: var(--gradient);
}
.chip.active .toggle span { left: 11px; }
.spacer { flex: 1; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes spin-reverse { to { transform: rotate(-360deg); } }
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .82; transform: scale(1.025); }
}
@keyframes orbit { to { transform: rotate(360deg); } }
+22
View File
@@ -0,0 +1,22 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "VynteConnect",
platforms: [
.macOS(.v14)
],
products: [
.executable(name: "VynteConnect", targets: ["VynteConnect"])
],
targets: [
.executableTarget(
name: "VynteConnect",
path: "Sources/VynteConnect",
resources: [
.process("../../Resources")
]
)
]
)
+77
View File
@@ -0,0 +1,77 @@
# Vynte Connect
Vynte Connect is a small macOS menu bar wrapper for NetBird. It gives internal Vynte users a branded one-button connect/disconnect flow for the self-hosted VPN at `https://vpn.vyntehome.com`.
The app does not implement VPN networking and does not store setup keys or NetBird secrets. It calls a bundled `netbird` CLI when one is packaged with the app, then falls back to an installed `netbird` CLI and polls the local NetBird daemon.
The app bundles Vynte visual assets and Montserrat for the menu bar popover UI. The UI is intentionally rudimentary: one flame button, a fixed Vynte gateway, connected stats, auto-connect, and quit.
## Requirements
- macOS 14 or newer.
- A bundled or installed NetBird CLI.
- A working NetBird daemon/network extension on the Mac.
- User belongs to the correct NetBird groups.
## Run from source
```bash
cd apps/vynte-connect
swift run
```
## Build the app bundle
```bash
cd apps/vynte-connect
./Scripts/build-app.sh
open "dist/Vynte Connect.app"
```
To force a specific NetBird binary into the app bundle:
```bash
cd apps/vynte-connect
NETBIRD_BINARY_PATH=/path/to/netbird ./Scripts/build-app.sh
```
The build script copies that binary to `dist/Vynte Connect.app/Contents/Resources/netbird`. If `NETBIRD_BINARY_PATH` is not set, it tries `Resources/netbird`, then the first `netbird` on `PATH`.
## Package an unsigned internal zip
```bash
cd apps/vynte-connect
./Scripts/package-zip.sh
```
Unsigned builds require users to approve the app in macOS Gatekeeper.
## Signed build
```bash
cd apps/vynte-connect
export DEVELOPER_ID_APPLICATION="Developer ID Application: Your Name (TEAMID)"
./Scripts/sign-app.sh
```
For notarization:
```bash
export APPLE_ID="you@example.com"
export APPLE_TEAM_ID="TEAMID"
export APPLE_APP_PASSWORD="app-specific-password"
./Scripts/notarize-zip.sh
```
## NetBird commands used
```bash
netbird up --management-url https://vpn.vyntehome.com --admin-url https://vpn.vyntehome.com
netbird down --management-url https://vpn.vyntehome.com
netbird status --json
netbird status --check ready
```
## Connection flow
Clicking the flame runs `netbird up` against `https://vpn.vyntehome.com`. Vynte Connect polls NetBird status until the tunnel is ready.
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="${ROOT_DIR}/.build/release"
DIST_DIR="${ROOT_DIR}/dist"
APP_NAME="Vynte Connect"
APP_DIR="${DIST_DIR}/${APP_NAME}.app"
EXECUTABLE_NAME="VynteConnect"
cd "$ROOT_DIR"
swift build -c release
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
cp "$BUILD_DIR/$EXECUTABLE_NAME" "$APP_DIR/Contents/MacOS/$EXECUTABLE_NAME"
RESOURCE_BUNDLE="${BUILD_DIR}/VynteConnect_VynteConnect.bundle"
if [[ -d "$RESOURCE_BUNDLE" ]]; then
cp -R "$RESOURCE_BUNDLE" "$APP_DIR/Contents/Resources/"
fi
NETBIRD_SOURCE="${NETBIRD_BINARY_PATH:-}"
if [[ -z "$NETBIRD_SOURCE" && -x "$ROOT_DIR/Resources/netbird" ]]; then
NETBIRD_SOURCE="$ROOT_DIR/Resources/netbird"
fi
if [[ -z "$NETBIRD_SOURCE" ]]; then
NETBIRD_SOURCE="$(command -v netbird || true)"
fi
if [[ -n "$NETBIRD_SOURCE" && -x "$NETBIRD_SOURCE" ]]; then
cp "$NETBIRD_SOURCE" "$APP_DIR/Contents/Resources/netbird"
chmod 755 "$APP_DIR/Contents/Resources/netbird"
else
echo "warning: no netbird binary found to bundle; set NETBIRD_BINARY_PATH=/path/to/netbird" >&2
fi
cat > "$APP_DIR/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Vynte Connect</string>
<key>CFBundleExecutable</key>
<string>VynteConnect</string>
<key>CFBundleIdentifier</key>
<string>com.vyntehome.connect</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Vynte Connect</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLIST
echo "Built ${APP_DIR}"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
: "${APPLE_ID:?Set APPLE_ID to the Apple ID used for notarization.}"
: "${APPLE_TEAM_ID:?Set APPLE_TEAM_ID to your Apple team ID.}"
: "${APPLE_APP_PASSWORD:?Set APPLE_APP_PASSWORD to an app-specific password or keychain profile password.}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ZIP_PATH="${ROOT_DIR}/dist/Vynte-Connect-macOS.zip"
"${ROOT_DIR}/Scripts/sign-app.sh"
SKIP_BUILD=1 PRESERVE_SIGNATURE=1 "${ROOT_DIR}/Scripts/package-zip.sh"
xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
echo "Notarized ${ZIP_PATH}"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="${ROOT_DIR}/dist/Vynte Connect.app"
ZIP_PATH="${ROOT_DIR}/dist/Vynte-Connect-macOS.zip"
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
"${ROOT_DIR}/Scripts/build-app.sh"
fi
if [[ "${PRESERVE_SIGNATURE:-0}" != "1" ]]; then
codesign --force --deep --sign - "$APP_DIR"
fi
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
rm -f "$ZIP_PATH"
ditto -c -k --keepParent "$APP_DIR" "$ZIP_PATH"
echo "Packaged ${ZIP_PATH}"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
: "${DEVELOPER_ID_APPLICATION:?Set DEVELOPER_ID_APPLICATION to your Apple Developer ID Application signing identity.}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="${ROOT_DIR}/dist/Vynte Connect.app"
if [[ ! -d "$APP_DIR" ]]; then
"${ROOT_DIR}/Scripts/build-app.sh"
fi
if [[ -f "$APP_DIR/Contents/Resources/netbird" ]]; then
codesign \
--force \
--options runtime \
--timestamp \
--sign "$DEVELOPER_ID_APPLICATION" \
"$APP_DIR/Contents/Resources/netbird"
fi
codesign \
--force \
--deep \
--options runtime \
--timestamp \
--sign "$DEVELOPER_ID_APPLICATION" \
"$APP_DIR"
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
echo "Signed ${APP_DIR}"
@@ -0,0 +1,9 @@
import Foundation
enum AppConfig {
static let appName = "Vynte Connect"
static let managementURL = URL(string: "https://vpn.vyntehome.com")!
static let gatewayHost = managementURL.host() ?? "vpn.vyntehome.com"
static let installURL = URL(string: "https://docs.netbird.io/get-started/install")!
static let supportEmail = "vynte@vyntehome.com"
}
@@ -0,0 +1,72 @@
import CoreText
import AppKit
import SwiftUI
enum VynteBrand {
static let background = Color(red: 0.078, green: 0.086, blue: 0.102)
static let panel = Color(red: 0.078, green: 0.086, blue: 0.102)
static let panelElevated = Color(red: 0.110, green: 0.118, blue: 0.138)
static let text = Color(red: 0.961, green: 0.965, blue: 0.973)
static let muted = Color(red: 0.565, green: 0.600, blue: 0.639)
static let lowText = Color(red: 0.353, green: 0.365, blue: 0.400)
static let hairline = Color.white.opacity(0.06)
static let blue = Color(red: 0.118, green: 0.565, blue: 1.000)
static let cyan = Color(red: 0.180, green: 0.820, blue: 0.940)
static let orange = Color(red: 1.000, green: 0.478, blue: 0.102)
static let orangeLight = Color(red: 1.000, green: 0.592, blue: 0.278)
static let pink = Color(red: 0.902, green: 0.243, blue: 0.361)
static let purple = Color(red: 0.435, green: 0.169, blue: 0.682)
static let red = Color(red: 0.902, green: 0.243, blue: 0.361)
static let errorText = Color(red: 1.000, green: 0.541, blue: 0.612)
static let logoGradient = LinearGradient(
colors: [orange, pink, purple, blue],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
static let wallpaperGradient = LinearGradient(
colors: [
orange.opacity(0.55),
pink.opacity(0.45),
purple.opacity(0.40),
blue.opacity(0.45)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
static func font(size: CGFloat, weight: Font.Weight = .regular) -> Font {
.custom("Montserrat", size: size).weight(weight)
}
}
enum FontRegistrar {
static func registerBundledFonts() {
guard let url = Bundle.module.url(forResource: "Montserrat-VariableFont_wght", withExtension: "ttf") else {
return
}
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
}
struct BundledImage: View {
let name: String
let fileExtension: String
init(_ name: String, fileExtension: String = "png") {
self.name = name
self.fileExtension = fileExtension
}
var body: some View {
if let url = Bundle.module.url(forResource: name, withExtension: fileExtension),
let image = NSImage(contentsOf: url) {
Image(nsImage: image)
.resizable()
} else {
Color.clear
}
}
}
@@ -0,0 +1,351 @@
@preconcurrency import Foundation
struct CommandResult: Sendable {
let exitCode: Int32
let stdout: String
let stderr: String
var combinedOutput: String {
[stdout, stderr].filter { !$0.isEmpty }.joined(separator: "\n")
}
}
struct NetBirdStatus: Decodable {
struct ServiceStatus: Decodable {
let url: String?
let connected: Bool?
let error: String?
}
struct Peers: Decodable {
let total: Int?
let connected: Int?
}
let daemonStatus: String?
let management: ServiceStatus?
let signal: ServiceStatus?
let netbirdIp: String?
let fqdn: String?
let peers: Peers?
}
enum NetBirdServiceError: LocalizedError, Sendable {
case commandNotFound
case commandFailed(String)
case invalidStatus(String)
var errorDescription: String? {
switch self {
case .commandNotFound:
"vpn.vyntehome.com helper is not installed."
case .commandFailed(let message):
message
case .invalidStatus(let message):
message
}
}
}
struct NetBirdService {
let executableURL: URL
static func discover() -> NetBirdService? {
let bundledCandidates = [
Bundle.main.resourceURL?.appendingPathComponent("netbird").path,
Bundle.module.url(forResource: "netbird", withExtension: nil)?.path
].compactMap { $0 }
let candidates = bundledCandidates + [
"/opt/homebrew/bin/netbird",
"/usr/local/bin/netbird",
"/usr/bin/netbird",
"/Applications/NetBird.app/Contents/MacOS/netbird"
]
let fileManager = FileManager.default
if let match = candidates.first(where: { fileManager.isExecutableFile(atPath: $0) }) {
return NetBirdService(executableURL: URL(fileURLWithPath: match))
}
if let pathMatch = findOnPath("netbird") {
return NetBirdService(executableURL: URL(fileURLWithPath: pathMatch))
}
return nil
}
func status() async throws -> NetBirdStatus {
let result = try await run(["status", "--json"], timeout: 15)
guard result.exitCode == 0 else {
throw NetBirdServiceError.commandFailed(cleanError(result))
}
guard let data = result.stdout.data(using: .utf8) else {
throw NetBirdServiceError.invalidStatus("vpn.vyntehome.com returned an unreadable status response.")
}
do {
return try JSONDecoder().decode(NetBirdStatus.self, from: data)
} catch {
throw NetBirdServiceError.invalidStatus("vpn.vyntehome.com status could not be parsed.")
}
}
func readyCheck() async -> Bool {
let result = try? await run(["status", "--check", "ready"], timeout: 8)
return result?.exitCode == 0
}
func connect() async throws {
try await installServiceIfNeeded()
let result = try await run([
"up",
"--management-url", AppConfig.managementURL.absoluteString,
"--admin-url", AppConfig.managementURL.absoluteString
], timeout: 180)
guard result.exitCode == 0 else {
let message = cleanError(result)
if message.localizedCaseInsensitiveContains("already registered by a different User") ||
message.localizedCaseInsensitiveContains("peer login has expired") {
try await resetLocalState()
throw NetBirdServiceError.commandFailed("vpn.vyntehome.com had stale registration state. I reset the local state; click Connect again to register with NetBird.")
}
throw NetBirdServiceError.commandFailed(message)
}
}
func resetLocalState() async throws {
_ = try? await run(["down", "--management-url", AppConfig.managementURL.absoluteString], timeout: 20)
_ = try? await run(["deregister", "--management-url", AppConfig.managementURL.absoluteString, "--admin-url", AppConfig.managementURL.absoluteString], timeout: 45)
try await runPrivilegedShell([
shellQuoted(executableURL.path),
"service",
"stop"
].joined(separator: " "))
_ = try? await run(["state", "clean", "--all"], timeout: 20)
try await runPrivilegedShell("rm -rf /etc/netbird /var/root/Library/Application\\ Support/netbird /var/root/Library/Preferences/io.netbird.client.plist")
try await runPrivilegedShell([
shellQuoted(executableURL.path),
"service",
"start"
].joined(separator: " "))
}
func installServiceIfNeeded() async throws {
if await serviceIsAvailable() {
return
}
try await runPrivilegedShell([
shellQuoted(executableURL.path),
"service",
"install",
"--management-url", shellQuoted(AppConfig.managementURL.absoluteString),
"--admin-url", shellQuoted(AppConfig.managementURL.absoluteString),
"--log-level", "info"
].joined(separator: " "))
try await runPrivilegedShell([
shellQuoted(executableURL.path),
"service",
"start"
].joined(separator: " "))
for _ in 0..<20 {
if await serviceIsAvailable() {
return
}
try? await Task.sleep(for: .milliseconds(500))
}
throw NetBirdServiceError.commandFailed("vpn.vyntehome.com helper was installed, but it did not become ready.")
}
func disconnect() async throws {
let result = try await run([
"down",
"--management-url", AppConfig.managementURL.absoluteString
], timeout: 45)
guard result.exitCode == 0 else {
throw NetBirdServiceError.commandFailed(cleanError(result))
}
}
func logout() async throws {
_ = try? await run([
"down",
"--management-url", AppConfig.managementURL.absoluteString
], timeout: 30)
let result = try await run([
"deregister",
"--management-url", AppConfig.managementURL.absoluteString,
"--admin-url", AppConfig.managementURL.absoluteString
], timeout: 60)
guard result.exitCode == 0 else {
let message = cleanError(result)
if message.localizedCaseInsensitiveContains("not registered") ||
message.localizedCaseInsensitiveContains("no active profile") {
return
}
throw NetBirdServiceError.commandFailed(message)
}
}
private func cleanError(_ result: CommandResult) -> String {
let output = result.combinedOutput.trimmingCharacters(in: .whitespacesAndNewlines)
if output.isEmpty {
return "vpn.vyntehome.com exited with code \(result.exitCode)."
}
return output
}
private func run(_ arguments: [String], timeout: TimeInterval) async throws -> CommandResult {
try await CommandRunner.run(
executableURL: executableURL,
arguments: arguments,
timeout: timeout
)
}
private func serviceIsAvailable() async -> Bool {
if FileManager.default.fileExists(atPath: "/var/run/netbird.sock") {
return true
}
let statusResult = try? await run(["status", "--json"], timeout: 8)
return statusResult?.exitCode == 0
}
private func runPrivilegedShell(_ command: String) async throws {
let script = "do shell script \(appleScriptStringLiteral(command)) with administrator privileges"
let result = try await CommandRunner.run(
executableURL: URL(fileURLWithPath: "/usr/bin/osascript"),
arguments: ["-e", script],
timeout: 120
)
guard result.exitCode == 0 else {
throw NetBirdServiceError.commandFailed(cleanError(result))
}
}
private func shellQuoted(_ value: String) -> String {
"'\(value.replacingOccurrences(of: "'", with: "'\\''"))'"
}
private func appleScriptStringLiteral(_ value: String) -> String {
"\"\(value.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
}
private static func findOnPath(_ executable: String) -> String? {
let path = ProcessInfo.processInfo.environment["PATH"] ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
for directory in path.split(separator: ":") {
let candidate = "\(directory)/\(executable)"
if FileManager.default.isExecutableFile(atPath: candidate) {
return candidate
}
}
return nil
}
}
enum CommandRunner {
static func run(
executableURL: URL,
arguments: [String],
timeout: TimeInterval
) async throws -> CommandResult {
try await withCheckedThrowingContinuation { continuation in
let process = Process()
process.executableURL = executableURL
process.arguments = arguments
process.environment = processEnvironment()
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
let state = CommandState(continuation: continuation)
process.terminationHandler = { process in
let stdoutData = stdout.fileHandleForReading.readDataToEndOfFile()
let stderrData = stderr.fileHandleForReading.readDataToEndOfFile()
let result = CommandResult(
exitCode: process.terminationStatus,
stdout: String(data: stdoutData, encoding: .utf8) ?? "",
stderr: String(data: stderrData, encoding: .utf8) ?? ""
)
state.finish(.success(result))
}
do {
try process.run()
} catch {
state.finish(.failure(error))
return
}
DispatchQueue.global().asyncAfter(deadline: .now() + timeout) {
if state.shouldTerminate {
process.terminate()
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
if process.isRunning {
kill(process.processIdentifier, SIGKILL)
}
}
state.finish(.failure(NetBirdServiceError.commandFailed("vpn.vyntehome.com command timed out.")))
}
}
}
}
private static func processEnvironment() -> [String: String] {
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = [
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin"
].joined(separator: ":")
return environment
}
}
private final class CommandState: @unchecked Sendable {
private let lock = NSLock()
private var completed = false
private let continuation: CheckedContinuation<CommandResult, Error>
init(continuation: CheckedContinuation<CommandResult, Error>) {
self.continuation = continuation
}
var shouldTerminate: Bool {
lock.lock()
defer { lock.unlock() }
return !completed
}
func finish(_ result: Result<CommandResult, Error>) {
lock.lock()
guard !completed else {
lock.unlock()
return
}
completed = true
lock.unlock()
continuation.resume(with: result)
}
}
@@ -0,0 +1,487 @@
import SwiftUI
struct StatusPopover: View {
@ObservedObject var model: VynteConnectModel
var body: some View {
ZStack {
VynteBackground()
VStack(spacing: 0) {
header
hero
gatewayStrip
if model.isConnected {
statsRow
}
footer
}
.background(.black.opacity(0.10))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(VynteBrand.hairline, lineWidth: 1)
}
.shadow(color: .black.opacity(0.46), radius: 28, y: 18)
.padding(10)
}
.preferredColorScheme(.dark)
}
private var header: some View {
HStack(spacing: 10) {
BundledImage("VynteIcon")
.scaledToFit()
.frame(width: 22, height: 22)
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
VStack(alignment: .leading, spacing: 1) {
Text("Vynte Connect")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(VynteBrand.text)
Text(model.identityLabel)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(VynteBrand.muted)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer()
RefreshButton(isWorking: model.isWorking) {
model.refreshNow()
}
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.overlay(alignment: .bottom) {
Rectangle()
.fill(VynteBrand.hairline)
.frame(height: 1)
}
}
private var hero: some View {
VStack(spacing: 12) {
Button {
model.primaryAction()
} label: {
ConnectOrb(state: model.state)
}
.buttonStyle(.plain)
.disabled(model.isWorking && !model.isConnecting)
.help(model.primaryButtonTitle)
VStack(spacing: 4) {
HStack(spacing: 8) {
if model.isConnected {
StatusDot(color: VynteBrand.orange, glow: true, size: 8)
} else if model.isError {
StatusDot(color: VynteBrand.red, glow: false, size: 8)
}
Text(model.statusTitle)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(model.isError ? VynteBrand.errorText : VynteBrand.text)
}
Text(model.statusMessage)
.font(.system(size: 12, weight: .regular))
.foregroundStyle(VynteBrand.muted)
.multilineTextAlignment(.center)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 18)
}
.padding(.top, 20)
.padding(.bottom, 16)
}
private var gatewayStrip: some View {
HStack(spacing: 10) {
Image(systemName: "server.rack")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(VynteBrand.muted)
.frame(width: 28, height: 28)
.background(.white.opacity(0.04), in: RoundedRectangle(cornerRadius: 6, style: .continuous))
VStack(alignment: .leading, spacing: 2) {
Text("GATEWAY")
.font(.system(size: 10, weight: .medium))
.tracking(0.6)
.foregroundStyle(VynteBrand.lowText)
Text(AppConfig.gatewayHost)
.font(.system(size: 12.5, weight: .medium, design: .monospaced))
.foregroundStyle(VynteBrand.text)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer()
Text(model.isConnected ? "LIVE" : "OFFLINE")
.font(.system(size: 10, weight: .semibold))
.tracking(0.4)
.foregroundStyle(model.isConnected ? VynteBrand.orangeLight : VynteBrand.lowText)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(
model.isConnected ? VynteBrand.orange.opacity(0.14) : .white.opacity(0.06),
in: Capsule()
)
}
.padding(10)
.background(.white.opacity(0.04), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(VynteBrand.hairline, lineWidth: 1)
}
.padding(.horizontal, 14)
.padding(.bottom, 12)
}
private var statsRow: some View {
HStack(spacing: 0) {
StatCell(label: "Internal IP", value: model.netbirdIP ?? "Assigned")
Rectangle()
.fill(VynteBrand.hairline)
.frame(width: 1)
StatCell(label: "Uptime", value: model.uptimeLabel)
}
.background(.white.opacity(0.04), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(VynteBrand.hairline, lineWidth: 1)
}
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.padding(.horizontal, 14)
.padding(.bottom, 12)
}
private var footer: some View {
HStack(spacing: 4) {
Button {
model.toggleAutoConnect()
} label: {
HStack(spacing: 6) {
Image(systemName: "bolt.fill")
.font(.system(size: 11, weight: .semibold))
Text("Auto-connect")
.font(.system(size: 11, weight: .medium))
TogglePill(isOn: model.autoConnectEnabled)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
}
.buttonStyle(VynteFooterChipStyle(active: model.autoConnectEnabled))
.help("Connect automatically when Vynte Connect starts")
Spacer()
if model.isConnected {
Button {
model.logout()
} label: {
Image(systemName: "person.crop.circle.badge.xmark")
.font(.system(size: 13, weight: .semibold))
.frame(width: 30, height: 26)
}
.buttonStyle(VynteIconButtonStyle())
.help("Log out and register with NetBird again")
}
Button {
model.quit()
} label: {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 13, weight: .semibold))
.frame(width: 30, height: 26)
}
.buttonStyle(VynteIconButtonStyle())
.help("Quit Vynte Connect")
}
.padding(.horizontal, 6)
.padding(.vertical, 8)
.overlay(alignment: .top) {
Rectangle()
.fill(VynteBrand.hairline)
.frame(height: 1)
}
}
}
struct VynteBackground: View {
var body: some View {
Color(red: 0.098, green: 0.106, blue: 0.124)
.overlay(.ultraThinMaterial.opacity(0.34))
.overlay {
VynteBrand.wallpaperGradient
.opacity(0.28)
}
}
}
struct RefreshButton: View {
let isWorking: Bool
let action: () -> Void
var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 30.0, paused: !isWorking)) { tl in
let angle = isWorking
? tl.date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 0.8) / 0.8 * 360
: 0.0
Button(action: action) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 13, weight: .semibold))
.frame(width: 26, height: 26)
.rotationEffect(.degrees(angle))
}
.buttonStyle(VynteIconButtonStyle())
.disabled(isWorking)
.help("Refresh status")
}
}
}
struct ConnectOrb: View {
let state: VPNConnectionState
private var isConnected: Bool {
if case .connected = state { return true }
return false
}
private var isConnecting: Bool {
if case .connecting = state { return true }
if case .checking = state { return true }
return false
}
private var isDisconnecting: Bool {
if case .disconnecting = state { return true }
if case .loggingOut = state { return true }
return false
}
private var isTransitioning: Bool {
isConnecting || isDisconnecting
}
private var isError: Bool {
if case .error = state { return true }
if case .netbirdMissing = state { return true }
return false
}
var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 60.0, paused: !isTransitioning)) { timeline in
let elapsed = timeline.date.timeIntervalSinceReferenceDate
let rotation = transitionRotation(elapsed)
ZStack {
Circle()
.fill(orbRing)
.frame(width: 116, height: 116)
.opacity(isError ? 0.58 : 1)
.shadow(color: activeGlow.opacity(glowOpacity), radius: 22)
.shadow(color: VynteBrand.purple.opacity(isConnected ? 0.24 : 0), radius: 38)
Circle()
.fill(
RadialGradient(
colors: [
Color(red: 0.122, green: 0.133, blue: 0.157),
Color(red: 0.078, green: 0.086, blue: 0.102)
],
center: UnitPoint(x: 0.30, y: 0.25),
startRadius: 4,
endRadius: 58
)
)
.frame(width: 112, height: 112)
if isTransitioning {
Circle()
.trim(from: transitionTrimStart, to: transitionTrimEnd)
.stroke(
AngularGradient(
colors: transitionGradientColors,
center: .center
),
style: StrokeStyle(lineWidth: 4, lineCap: .round)
)
.frame(width: 124, height: 124)
.rotationEffect(.degrees(rotation))
}
if isConnected {
BundledImage("VynteIcon")
.scaledToFit()
.frame(width: 52, height: 52)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
} else {
Image(systemName: stateIcon)
.font(.system(size: 38, weight: .regular))
.foregroundStyle(iconColor)
}
}
.frame(width: 126, height: 126)
}
}
private var orbRing: AnyShapeStyle {
if isConnected || isTransitioning {
return AnyShapeStyle(VynteBrand.logoGradient)
}
return AnyShapeStyle(
AngularGradient(
colors: [
Color(red: 0.165, green: 0.176, blue: 0.200),
Color(red: 0.235, green: 0.247, blue: 0.282),
Color(red: 0.165, green: 0.176, blue: 0.200)
],
center: .center,
angle: .degrees(90)
)
)
}
private var stateIcon: String {
switch state {
case .netbirdMissing:
"square.and.arrow.down"
case .error:
"exclamationmark"
case .loggingOut:
"person.crop.circle.badge.xmark"
default:
"power"
}
}
private var iconColor: Color {
if isError {
return VynteBrand.pink
}
if isTransitioning {
return VynteBrand.text
}
return Color(red: 0.478, green: 0.494, blue: 0.529)
}
private var activeGlow: Color {
isDisconnecting ? VynteBrand.purple : VynteBrand.orange
}
private var glowOpacity: Double {
isConnected ? 0.34 : 0
}
private var transitionTrimStart: CGFloat {
isDisconnecting ? 0.18 : 0.04
}
private var transitionTrimEnd: CGFloat {
isDisconnecting ? 0.52 : 0.74
}
private var transitionGradientColors: [Color] {
if isDisconnecting {
return [.clear, VynteBrand.blue.opacity(0.45), VynteBrand.purple, VynteBrand.pink.opacity(0.80)]
}
return [.clear, VynteBrand.orange, VynteBrand.pink, VynteBrand.blue]
}
private func transitionRotation(_ elapsed: TimeInterval) -> Double {
let duration = isDisconnecting ? 1.35 : 1.05
let progress = elapsed.truncatingRemainder(dividingBy: duration) / duration
return (isDisconnecting ? -360 : 360) * progress
}
}
struct StatusDot: View {
let color: Color
let glow: Bool
let size: CGFloat
var body: some View {
Circle()
.fill(color)
.frame(width: size, height: size)
.shadow(color: glow ? color.opacity(0.55) : .clear, radius: 8)
.overlay {
if glow {
Circle()
.stroke(color.opacity(0.18), lineWidth: 6)
}
}
}
}
struct StatCell: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text(label.uppercased())
.font(.system(size: 10, weight: .medium))
.tracking(0.6)
.foregroundStyle(VynteBrand.lowText)
Text(value)
.font(.system(size: 12.5, weight: .medium, design: .monospaced))
.foregroundStyle(VynteBrand.text)
.lineLimit(1)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 9)
.background(.black.opacity(0.18))
}
}
struct TogglePill: View {
let isOn: Bool
var body: some View {
ZStack(alignment: isOn ? .trailing : .leading) {
Capsule()
.fill(isOn ? AnyShapeStyle(VynteBrand.logoGradient) : AnyShapeStyle(.white.opacity(0.10)))
.frame(width: 22, height: 12)
Circle()
.fill(.white)
.frame(width: 9, height: 9)
.padding(.horizontal, 1.5)
}
}
}
struct VynteIconButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(configuration.isPressed ? VynteBrand.text : VynteBrand.muted)
.background(configuration.isPressed ? .white.opacity(0.08) : .clear, in: RoundedRectangle(cornerRadius: 6))
}
}
struct VynteFooterChipStyle: ButtonStyle {
let active: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(active ? VynteBrand.orangeLight : VynteBrand.muted)
.background(
active ? VynteBrand.orange.opacity(configuration.isPressed ? 0.16 : 0.10) : .white.opacity(configuration.isPressed ? 0.06 : 0),
in: RoundedRectangle(cornerRadius: 6, style: .continuous)
)
}
}
@@ -0,0 +1,47 @@
import AppKit
import SwiftUI
@main
struct VynteConnectApp: App {
@StateObject private var model = VynteConnectModel()
init() {
FontRegistrar.registerBundledFonts()
}
var body: some Scene {
MenuBarExtra {
StatusPopover(model: model)
.frame(width: 384)
} label: {
VynteMenuBarIcon()
.accessibilityLabel("Vynte Connect")
}
.menuBarExtraStyle(.window)
}
}
struct VynteMenuBarIcon: View {
var body: some View {
if let image = Self.image {
Image(nsImage: image)
.resizable()
.renderingMode(.original)
.scaledToFit()
.frame(width: 18, height: 18)
} else {
Image(systemName: "flame.fill")
.font(.system(size: 14, weight: .semibold))
}
}
private static var image: NSImage? {
guard let url = Bundle.module.url(forResource: "VynteMenuBarIcon", withExtension: "png"),
let image = NSImage(contentsOf: url) else {
return nil
}
image.size = NSSize(width: 18, height: 18)
image.isTemplate = false
return image
}
}
@@ -0,0 +1,480 @@
import AppKit
import Foundation
enum VPNConnectionState: Equatable {
case checking
case netbirdMissing
case disconnected
case connecting
case disconnecting
case connected
case loggingOut
case error(String)
}
@MainActor
final class VynteConnectModel: ObservableObject {
@Published private(set) var state: VPNConnectionState = .checking
@Published private(set) var netbirdPath: String?
@Published private(set) var netbirdIP: String?
@Published private(set) var fqdn: String?
@Published private(set) var connectedPeers: Int = 0
@Published private(set) var totalPeers: Int = 0
@Published private(set) var lastUpdated: Date?
@Published private(set) var autoConnectEnabled: Bool
@Published private(set) var connectedSince: Date?
@Published private(set) var isCompletingRegistration = false
private var service: NetBirdService?
private var refreshTask: Task<Void, Never>?
private var connectTask: Task<Void, Never>?
private var logoutTask: Task<Void, Never>?
private var hasAttemptedStartupAutoConnect = false
private let defaults = UserDefaults.standard
private static let autoConnectDefaultsKey = "VynteConnect.autoConnectEnabled"
init() {
autoConnectEnabled = defaults.bool(forKey: Self.autoConnectDefaultsKey)
refreshTask = Task { [weak self] in
await self?.refreshLoop()
}
}
deinit {
refreshTask?.cancel()
connectTask?.cancel()
logoutTask?.cancel()
}
var menuBarSystemImage: String {
switch state {
case .checking:
"circle.dotted"
case .netbirdMissing:
"exclamationmark.triangle"
case .disconnected:
"flame"
case .connecting:
"arrow.triangle.2.circlepath"
case .disconnecting:
"arrow.triangle.2.circlepath"
case .connected:
"flame.fill"
case .loggingOut:
"rectangle.portrait.and.arrow.right"
case .error:
"exclamationmark.octagon"
}
}
var primaryButtonTitle: String {
switch state {
case .connected:
"Disconnect"
case .connecting:
"Connecting..."
case .disconnecting:
"Disconnecting..."
case .netbirdMissing:
"Install helper"
case .loggingOut:
"Logging out..."
default:
"Connect"
}
}
var statusTitle: String {
switch state {
case .checking:
"Checking vpn.vyntehome.com"
case .netbirdMissing:
"vpn.vyntehome.com missing"
case .disconnected:
"Disconnected"
case .connecting:
"Connecting..."
case .disconnecting:
"Disconnecting..."
case .connected:
"Connected"
case .loggingOut:
"Logging out"
case .error:
"Connection failed"
}
}
var statusMessage: String {
switch state {
case .checking:
return "Preparing the Vynte access client."
case .netbirdMissing:
return "vpn.vyntehome.com helper is not bundled with this build."
case .disconnected:
return "Click the flame to connect with NetBird."
case .connecting:
if isCompletingRegistration {
return "Finishing NetBird registration..."
}
return "Starting secure Vynte access..."
case .disconnecting:
return "Closing secure Vynte access..."
case .connected:
return "You're on the Vynte network."
case .loggingOut:
return "Removing this Mac's NetBird registration."
case .error(let message):
return message
}
}
var isConnected: Bool {
if case .connected = state {
return true
}
return false
}
var isWorking: Bool {
if case .checking = state {
return true
}
if case .connecting = state {
return true
}
if case .disconnecting = state {
return true
}
if case .loggingOut = state {
return true
}
return false
}
var isConnecting: Bool {
if case .connecting = state {
return true
}
return false
}
var isDisconnecting: Bool {
if case .disconnecting = state {
return true
}
return false
}
var isError: Bool {
if case .error = state {
return true
}
if case .netbirdMissing = state {
return true
}
return false
}
var identityLabel: String {
if let fqdn {
return fqdn
}
let user = NSUserName()
if user.isEmpty {
return "Vynte access"
}
return "\(user)@vynte"
}
var uptimeLabel: String {
guard let connectedSince else {
return "00:00:00"
}
let elapsed = max(0, Int(Date().timeIntervalSince(connectedSince)))
let hours = elapsed / 3600
let minutes = (elapsed % 3600) / 60
let seconds = elapsed % 60
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
func primaryAction() {
switch state {
case .netbirdMissing:
NSWorkspace.shared.open(AppConfig.installURL)
case .connected:
disconnect()
case .connecting, .disconnecting, .checking, .loggingOut:
break
default:
connect()
}
}
func logout() {
connectTask?.cancel()
logoutTask?.cancel()
isCompletingRegistration = false
state = .loggingOut
logoutTask = Task { [weak self] in
guard let self else { return }
await self.ensureService()
guard let service = self.service else {
await MainActor.run { self.state = .netbirdMissing }
return
}
do {
try await service.logout()
await MainActor.run {
self.clearStatusFields()
self.state = .disconnected
}
} catch {
await MainActor.run {
self.state = .error(Self.userFacingError(error))
}
}
}
}
func refreshNow() {
Task {
await refresh()
}
}
func toggleAutoConnect() {
autoConnectEnabled.toggle()
defaults.set(autoConnectEnabled, forKey: Self.autoConnectDefaultsKey)
}
func quit() {
NSApplication.shared.terminate(nil)
}
private func connect() {
connectTask?.cancel()
isCompletingRegistration = false
state = .connecting
connectTask = Task { [weak self] in
guard let self else { return }
await self.ensureService()
guard let service = self.service else {
await MainActor.run { self.state = .netbirdMissing }
return
}
do {
try await service.connect()
await MainActor.run {
self.isCompletingRegistration = false
}
await self.waitForReady()
} catch {
await MainActor.run {
self.state = .error(Self.userFacingError(error))
}
}
}
}
private func disconnect() {
connectTask?.cancel()
isCompletingRegistration = false
state = .disconnecting
Task { [weak self] in
guard let self else { return }
guard let service = self.service ?? NetBirdService.discover() else {
await MainActor.run { self.state = .netbirdMissing }
return
}
do {
try await service.disconnect()
await MainActor.run {
self.connectedSince = nil
}
await self.refresh()
} catch {
await MainActor.run {
self.state = .error(Self.userFacingError(error))
}
}
}
}
private func refreshLoop() async {
await refresh()
await runStartupAutoConnectIfNeeded()
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(12))
await refresh()
}
}
private func ensureService() async {
if service == nil {
service = NetBirdService.discover()
netbirdPath = service?.executableURL.path
}
}
private func refresh() async {
await ensureService()
guard let service else {
state = .netbirdMissing
return
}
do {
let status = try await service.status()
updateStatusFields(status)
lastUpdated = Date()
if connectionIsActive(status) {
state = .connected
isCompletingRegistration = false
if connectedSince == nil {
connectedSince = Date()
}
} else {
state = .disconnected
isCompletingRegistration = false
connectedSince = nil
}
} catch {
isCompletingRegistration = false
connectedSince = nil
let message = Self.userFacingError(error)
if message.localizedCaseInsensitiveContains("daemon") || message.localizedCaseInsensitiveContains("service") {
state = .error("Click the flame to install the Vynte network helper.")
} else {
state = .error(message)
}
}
}
private func waitForReady() async {
guard let service else {
state = .netbirdMissing
return
}
for _ in 0..<60 {
do {
let status = try await service.status()
updateStatusFields(status)
lastUpdated = Date()
if connectionIsActive(status) {
state = .connected
isCompletingRegistration = false
if connectedSince == nil {
connectedSince = Date()
}
return
}
if let staleRegistrationMessage = staleRegistrationMessage(status) {
try? await service.resetLocalState()
isCompletingRegistration = false
state = .error(staleRegistrationMessage)
return
}
isCompletingRegistration = registrationIsPending(status)
} catch {
// During registration, the daemon can briefly return partial status.
}
if await service.readyCheck() {
await refresh()
if isConnected {
return
}
}
try? await Task.sleep(for: .seconds(2))
}
isCompletingRegistration = false
state = .error("NetBird registration did not finish. Try Connect again.")
}
private func runStartupAutoConnectIfNeeded() async {
guard autoConnectEnabled, !hasAttemptedStartupAutoConnect else {
return
}
hasAttemptedStartupAutoConnect = true
if case .disconnected = state {
connect()
}
}
private func updateStatusFields(_ status: NetBirdStatus) {
netbirdIP = status.netbirdIp
fqdn = status.fqdn
connectedPeers = status.peers?.connected ?? 0
totalPeers = status.peers?.total ?? 0
}
private func clearStatusFields() {
netbirdIP = nil
fqdn = nil
connectedPeers = 0
totalPeers = 0
connectedSince = nil
lastUpdated = Date()
isCompletingRegistration = false
}
private func connectionIsActive(_ status: NetBirdStatus) -> Bool {
let managementConnected = status.management?.connected == true
let signalConnected = status.signal?.connected == true
let daemonConnected = status.daemonStatus?.localizedCaseInsensitiveContains("connected") == true
let hasAssignedIdentity = (status.netbirdIp?.isEmpty == false) || (status.fqdn?.isEmpty == false)
return daemonConnected && (managementConnected || signalConnected || hasAssignedIdentity)
}
private func staleRegistrationMessage(_ status: NetBirdStatus) -> String? {
let errors = [
status.management?.error,
status.signal?.error
].compactMap { $0 }.joined(separator: "\n")
if errors.localizedCaseInsensitiveContains("already registered by a different User") {
return "vpn.vyntehome.com had stale registration state from a previous NetBird login. I reset the local state; click Connect again to register with NetBird."
}
return nil
}
private func registrationIsPending(_ status: NetBirdStatus) -> Bool {
if status.daemonStatus?.localizedCaseInsensitiveContains("needslogin") == true {
return true
}
let errors = [
status.management?.error,
status.signal?.error
].compactMap { $0 }.joined(separator: "\n")
return errors.localizedCaseInsensitiveContains("please log in") ||
errors.localizedCaseInsensitiveContains("no peer auth method provided")
}
private static func userFacingError(_ error: Error) -> String {
if let localized = (error as? LocalizedError)?.errorDescription, !localized.isEmpty {
return localized
}
return error.localizedDescription
}
}
+84
View File
@@ -0,0 +1,84 @@
# Vynte Connect Design Spec
## Purpose
Vynte Connect is an internal macOS menu bar app for connecting to the VynteHome NetBird VPN. It should feel obvious to non-infrastructure users: open app, sign in, connect, then open internal apps.
## Visual Direction
Use the Vynte pitch deck style:
- Dark base with soft blue, orange, and pink warmth.
- Flame icon asset from `icon.png` as the app and menu bar logo.
- Montserrat typography.
- Minimal copy focused on comfort, home, and secure team access.
## Frames
### First Launch
- Header: VYNTE mark and wordmark.
- Title: `Private access for the team`
- Body: `Connect to VynteHome to open internal apps securely.`
- Primary action: `Connect to VynteHome`
- Secondary note: `Authentication opens in your browser.`
### NetBird Missing
- Title: `NetBird required`
- Body: `Vynte Connect uses the official NetBird client securely underneath. Install NetBird, then come back here.`
- Primary action: `Install NetBird`
- Secondary action: `Refresh status`
### Disconnected
- Status: gray dot.
- Title: `Ready to connect`
- Body: `Connect to open Vynte internal apps securely.`
- Primary action: `Connect to VynteHome`
- Help bullets:
- `Authentication opens in your browser.`
- `Internal apps unlock after the tunnel connects.`
- `Vynte never stores your password.`
### Connecting
- Status: orange dot and spinner.
- Title: `Authenticating`
- Body: `Finishing NetBird registration...`
- Primary action disabled: `Connecting...`
### Connected
- Status: green dot.
- Title: `Connected`
- Body: `Secure tunnel active as <NetBird IP>.`
- Primary action: `Disconnect`
- App links:
- `Vynte Home` -> `https://vynte.internal.vyntehome.com`
- `Plane` -> `https://plane.internal.zachsharma.us`
- Show small reachability dots for internal links.
### Error
- Status: red dot.
- Title: `Needs attention`
- Body: concise error explanation.
- Primary action changes based on error:
- Missing NetBird: `Install NetBird`
- Other errors: `Connect to VynteHome`
- Footer always includes `Dashboard`, `Refresh`, and `Quit`.
## Figma Notes
Create a compact screen set, not a full design system. Suggested frame size: `384 x 560`, matching the app popover. Components to include:
- Vynte mark and wordmark lockup.
- Status card.
- Primary button.
- Internal app link row.
- Error/help bullet row.
Use the pitch deck background asset as a reference, but keep the actual app UI native SwiftUI so it remains lightweight.
The browser-reviewable mockup sheet is in `docs/vynte-connect-mockups.html`.
+80
View File
@@ -0,0 +1,80 @@
import { createWriteStream, existsSync, mkdirSync, rmSync } from "node:fs";
import { basename, join } from "node:path";
import { pipeline } from "node:stream/promises";
import { spawnSync } from "node:child_process";
const root = join(import.meta.dirname, "..");
const versionInput = process.env.NETBIRD_VERSION || "latest";
const release = await getRelease(versionInput);
const version = release.tag_name.replace(/^v/, "");
const tmpDir = join(root, ".tmp-netbird-assets");
const targets = [
{
label: "macOS arm64",
assetName: `netbird_${version}_darwin_arm64.tar.gz`,
outputDir: join(root, "apps", "vynte-connect", "Resources"),
files: [{ source: "netbird", destination: "netbird" }]
},
{
label: "Windows amd64",
assetName: `netbird_${version}_windows_amd64_signed.tar.gz`,
outputDir: join(root, "apps", "vynte-connect-windows", "resources"),
files: [
{ source: "netbird.exe", destination: "netbird.exe" },
{ source: "wintun.dll", destination: "wintun.dll" }
]
}
];
rmSync(tmpDir, { recursive: true, force: true });
mkdirSync(tmpDir, { recursive: true });
for (const target of targets) {
const asset = release.assets.find((candidate) => candidate.name === target.assetName);
if (!asset) {
throw new Error(`Could not find ${target.assetName} in NetBird ${release.tag_name}.`);
}
const archivePath = join(tmpDir, asset.name);
await download(asset.browser_download_url, archivePath);
const extractDir = join(tmpDir, basename(asset.name, ".tar.gz"));
mkdirSync(extractDir, { recursive: true });
run("tar", ["-xzf", archivePath, "-C", extractDir]);
mkdirSync(target.outputDir, { recursive: true });
for (const file of target.files) {
run("cp", [join(extractDir, file.source), join(target.outputDir, file.destination)]);
}
console.log(`Installed ${target.label} NetBird ${release.tag_name} assets.`);
}
rmSync(tmpDir, { recursive: true, force: true });
async function getRelease(versionSelector) {
const url = versionSelector === "latest"
? "https://api.github.com/repos/netbirdio/netbird/releases/latest"
: `https://api.github.com/repos/netbirdio/netbird/releases/tags/${versionSelector.startsWith("v") ? versionSelector : `v${versionSelector}`}`;
const response = await fetch(url, { headers: { "User-Agent": "vynte-connect-build" } });
if (!response.ok) {
throw new Error(`NetBird release lookup failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
async function download(url, destination) {
const response = await fetch(url, { headers: { "User-Agent": "vynte-connect-build" } });
if (!response.ok || !response.body) {
throw new Error(`Download failed for ${url}: ${response.status} ${response.statusText}`);
}
await pipeline(response.body, createWriteStream(destination));
}
function run(command, args) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}`);
}
}