Compare commits

...
1 Commits
Author SHA1 Message Date
thePR0M3TH3AN 2b29d71ac5 chore: release v0.3.4 2026-03-17 14:18:42 -04:00
15 changed files with 150 additions and 16 deletions

No files matched your search

+1
View File
@@ -37,6 +37,7 @@ Run the following commands to verify your changes:
- **Dependencies**: if you add a new Python library to `requirements.txt`, you MUST run `voxvera vendorize` to include it in the portable distribution.
- **Layout Integrity**: VoxVera uses visual width validation. If you add new UI elements, ensure they respect the physical boundaries of an 8.5"x11" flyer.
- **Tests**: Ensure all tests pass: `pytest tests/`. Run the integrity suite specifically when modifying locales: `pytest tests/test_integrity.py`.
- **Recommended local check**: Run `bash scripts/check.sh` before opening a PR. It mirrors the main lint/test/build checks in one command.
- **Style**: Follow the existing coding style (PEP 8 for Python).
- **Documentation**: If adding a new feature, update the master templates in `docs/templates/` and run `voxvera build-docs`.
+7
View File
@@ -30,6 +30,13 @@ pip install .
voxvera vendorize # Optional: pull dependencies locally for offline use
```
### 5. Local Validation
Run the full local validation sweep with:
```bash
bash scripts/check.sh
```
This runs linting, Python tests, Electron smoke tests, dependency checks, and the docs/site rebuild steps used during development.
---
## Key Features
+13 -4
View File
@@ -15,9 +15,12 @@ echo "Starting E2E verification at $(date)"
if [ -n "${VOXVERA_BIN:-}" ] && [ -f "$VOXVERA_BIN" ]; then
voxvera_cmd="$VOXVERA_BIN"
echo "Using provided VoxVera binary: $voxvera_cmd" >&2
elif command -v voxvera >/dev/null 2>&1; then
voxvera_cmd="$(command -v voxvera)"
echo "Using existing VoxVera on PATH: $voxvera_cmd" >&2
else
echo "Installing VoxVera in editable mode..."
python3 -m pip install --break-system-packages -e ".[e2e]"
python3 -m pip install --break-system-packages --no-build-isolation -e ".[e2e]"
voxvera_cmd="$(command -v voxvera || echo "$HOME/.local/bin/voxvera")"
fi
@@ -30,16 +33,22 @@ DemoUser
voxvera
EOI
config_path="$HOME/host/voxvera/config.json"
if [ ! -f "$config_path" ]; then
echo "CRITICAL: Expected config file not found after init: $config_path" >&2
exit 1
fi
echo "Building demo flyer..."
"$voxvera_cmd" --lang en build
"$voxvera_cmd" --lang en --config "$config_path" build
# Verify build output exists in host/
folder_name="voxvera"
host_dir="voxvera/host/$folder_name"
host_dir="$HOME/host/$folder_name"
if [ ! -d "$host_dir" ]; then
echo "CRITICAL: Host directory $host_dir not found!" >&2
ls -R voxvera/host/ || true
ls -R "$HOME/host/" || true
exit 1
fi
+10 -5
View File
@@ -4,6 +4,7 @@ const path = require('path');
const which = require('which');
const fs = require('fs');
const { launchTor } = require('./tor.js');
const { extractOnionUrl } = require('./runtime-utils');
let mainWindow;
let onionProc;
@@ -74,8 +75,12 @@ async function runServe (retry = false) {
onionProc.stdout.on('data', buf => {
const line = buf.toString();
const m = line.match(/OnionShare is hosting at (http.*\.onion)/);
if (m) { gotURL = true; clearTimeout(softTimeout); }
const onionUrl = extractOnionUrl(line);
if (onionUrl) {
gotURL = true;
clearTimeout(softTimeout);
mainWindow.webContents.send('onion-url', onionUrl);
}
mainWindow.webContents.send('log', { text: line });
});
@@ -134,9 +139,9 @@ ipcMain.handle('run-quickstart', async (_, config) => {
if (mainWindow) {
mainWindow.webContents.send('log', { text: line, isError: false });
}
const m = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
if (m && mainWindow) {
mainWindow.webContents.send('onion-url', m[1]);
const onionUrl = extractOnionUrl(line);
if (onionUrl && mainWindow) {
mainWindow.webContents.send('onion-url', onionUrl);
}
});
proc.stderr.on('data', data => {
+2 -1
View File
@@ -4,7 +4,8 @@
"main": "main.js",
"scripts": {
"start": "electron .",
"lint": "echo 'lint pass'"
"lint": "echo 'lint pass'",
"test": "node --test runtime-utils.test.js"
},
"devDependencies": {
"electron": "^29.0.0",
+16
View File
@@ -0,0 +1,16 @@
function extractOnionUrl(line) {
const match = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i)
|| line.match(/OnionShare is hosting at\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
return match ? match[1] : null;
}
function platformDirName(platform = process.platform) {
if (platform === 'win32') return 'win';
if (platform === 'darwin') return 'mac';
return 'linux';
}
module.exports = {
extractOnionUrl,
platformDirName,
};
+28
View File
@@ -0,0 +1,28 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { extractOnionUrl, platformDirName } = require('./runtime-utils');
test('extractOnionUrl handles current CLI output', () => {
assert.equal(
extractOnionUrl('[voxvera] Onion URL: http://test123.onion'),
'http://test123.onion'
);
});
test('extractOnionUrl handles legacy OnionShare output', () => {
assert.equal(
extractOnionUrl('OnionShare is hosting at http://legacy456.onion'),
'http://legacy456.onion'
);
});
test('extractOnionUrl returns null when no onion URL is present', () => {
assert.equal(extractOnionUrl('still bootstrapping'), null);
});
test('platformDirName maps bundled resource folders correctly', () => {
assert.equal(platformDirName('linux'), 'linux');
assert.equal(platformDirName('darwin'), 'mac');
assert.equal(platformDirName('win32'), 'win');
});
+4 -2
View File
@@ -3,14 +3,16 @@ const path = require('path');
const fs = require('fs');
const which = require('which');
const getPort = (...args) => import('get-port').then(m => m.default(...args));
const { platformDirName } = require('./runtime-utils');
async function launchTor() {
const socks = await getPort();
const control = await getPort();
const torBase = path.join(__dirname, '..', '..', 'voxvera', 'resources', 'tor');
let exe = path.join(torBase, process.platform,
const platformDir = platformDirName(process.platform);
let exe = path.join(torBase, platformDir,
process.platform === 'win32' ? 'tor.exe' : 'tor');
let obfs4 = path.join(torBase, process.platform,
let obfs4 = path.join(torBase, platformDir,
process.platform === 'win32' ? 'obfs4proxy.exe' : 'obfs4proxy');
const missing = p => {
+1 -1
View File
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
[project]
name = "voxvera"
version = "0.3.3"
version = "0.3.4"
description = "VoxVera CLI utilities"
readme = "README.md"
authors = [{name = "VoxVera"}]
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
echo "==> flake8"
python3 -m flake8 voxvera tests \
--jobs=1 \
--exclude=voxvera/vendor \
--ignore=E501,W503,W293,E402,E306,E704,E261,F841,E302,F401,E401,F541
echo "==> pytest"
python3 -m pytest tests/ -v
echo "==> electron smoke tests"
(cd gui/electron && npm test)
echo "==> dependency check"
python3 -m voxvera.cli --lang en check
echo "==> build docs"
python3 -m voxvera.cli build-docs
echo "==> build site"
python3 -m voxvera.cli build-site
echo "All local checks passed."
Binary file not shown.
Binary file not shown.
+15
View File
@@ -126,6 +126,21 @@ def test_import_preserves_session(tmp_path, monkeypatch):
assert (host_dir / "index.html").exists()
def test_import_rejects_zip_path_traversal(tmp_path, monkeypatch, capsys):
_setup_tmp(monkeypatch, tmp_path)
malicious_zip = tmp_path / "malicious.zip"
with zipfile.ZipFile(malicious_zip, "w") as zf:
zf.writestr("../escape.txt", "owned")
zf.writestr("config.json", json.dumps({"folder_name": "evil"}))
cli.import_site(str(malicious_zip))
captured = capsys.readouterr()
assert "Refusing to extract path outside destination" in captured.out
assert not (tmp_path / "escape.txt").exists()
assert not ((tmp_path / "data" / "host" / "evil").exists())
def test_check_all_present(capsys, monkeypatch):
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/" + cmd)
cli.main(["check"])
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
__version__ = "0.3.3"
__version__ = "0.3.4"
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
# For pip/pipx installs the system/venv packages are correct and the vendored
+25 -2
View File
@@ -28,6 +28,8 @@ from InquirerPy import prompt, inquirer
from rich.console import Console
from voxvera import __version__
GITHUB_RECOMMENDED_BINARY_LIMIT = 95 * 1024 * 1024
# package root (contains bundled templates and src/)
if getattr(sys, 'frozen', False):
ROOT = Path(sys._MEIPASS).joinpath("voxvera")
@@ -858,6 +860,19 @@ import zipfile
import tempfile
def _safe_extract_zip(zipf: zipfile.ZipFile, dest_dir: Path) -> None:
"""Extract a ZIP archive without allowing path traversal."""
dest_dir = dest_dir.resolve()
for member in zipf.infolist():
member_path = Path(member.filename)
if member_path.is_absolute():
raise ValueError(f"Refusing to extract absolute path from archive: {member.filename}")
target_path = (dest_dir / member_path).resolve()
if not str(target_path).startswith(str(dest_dir) + os.sep) and target_path != dest_dir:
raise ValueError(f"Refusing to extract path outside destination: {member.filename}")
zipf.extractall(dest_dir)
def get_export_dir() -> Path:
"""Get the default export directory (~/voxvera-exports)."""
export_dir = Path.home() / "voxvera-exports"
@@ -911,7 +926,11 @@ def import_site(zip_path: str):
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(zip_path, 'r') as zipf:
zipf.extractall(tmpdir)
try:
_safe_extract_zip(zipf, Path(tmpdir))
except ValueError as e:
print(f"Error: Invalid export file. {e}")
return
tmp_path = Path(tmpdir)
config_file = tmp_path / "config.json"
@@ -1185,11 +1204,15 @@ def build_site():
# Bundle source version
bundle_source(download_dir / "voxvera-source.zip")
# Copy standalone binaries if they exist
# Copy standalone binaries if they exist, but avoid writing oversized
# artifacts into the tracked site tree where GitHub rejects >100 MB files.
bin_dir = ROOT / "resources" / "bin"
if bin_dir.exists():
for bin_file in bin_dir.iterdir():
if bin_file.is_file():
if bin_file.stat().st_size > GITHUB_RECOMMENDED_BINARY_LIMIT:
print(f"Skipping oversized site download artifact: {bin_file.name}")
continue
shutil.copy(bin_file, download_dir / bin_file.name)
print("site/ directory successfully synchronized.")