Mobile companion app + paired-device backend

New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs:
Captures (the hero — floating gold mic, live mic-meter waveform,
expand-row playback), Generate (profile picker + speak + autoplay +
recent), and Voices (searchable profile list).

Pairing (V0): backend mints a one-time token, mobile scans/pastes the
voicebox:// URL, server returns a long-lived bearer it stores only as a
SHA-256 hash. Bearer-or-loopback auth applied to every user-data router
so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers
(the desktop app) keep their friction-free access.

Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto-
detected via the App-bundle binary path on macOS), QR rendering,
5-minute expiry countdown, copyable URL fallback, paired-device list
with revoke. Auto-closes when a new device pairs.

just dev now binds the backend to 0.0.0.0 so paired phones can reach
it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh
Apple Silicon worktrees get a working STT path on first install.
This commit is contained in:
James Pine
2026-04-25 17:09:32 -07:00
parent 2bcb98d1a8
commit f4d21504e3
49 changed files with 5229 additions and 30 deletions
+42 -19
View File
@@ -1,6 +1,23 @@
"""Route registration for the voicebox API."""
"""Route registration for the voicebox API.
from fastapi import FastAPI
Authentication model
--------------------
Two router groups:
* **Open** — ``health`` (status checks anyone on the LAN may probe) and
``pairing`` (pre-pair endpoints + admin endpoints with their own
loopback-only or token-only gates).
* **Protected** — everything else, gated by ``require_bearer_or_loopback``:
loopback callers (the desktop app over 127.0.0.1) pass without auth as
before; LAN/Tailscale callers must present a valid paired-device bearer.
This is what lets ``just dev`` bind to 0.0.0.0 without exposing user
data to anyone on the same network.
"""
from fastapi import Depends, FastAPI
from ..utils.auth import require_bearer_or_loopback
def register_routers(app: FastAPI) -> None:
@@ -23,22 +40,28 @@ def register_routers(app: FastAPI) -> None:
from .speak import router as speak_router
from .mcp_bindings import router as mcp_bindings_router
from .events import router as events_router
from .pairing import router as pairing_router
# Open — health probes and the pre-pair / admin pairing endpoints.
app.include_router(health_router)
app.include_router(profiles_router)
app.include_router(channels_router)
app.include_router(generations_router)
app.include_router(history_router)
app.include_router(transcription_router)
app.include_router(llm_router)
app.include_router(captures_router)
app.include_router(stories_router)
app.include_router(effects_router)
app.include_router(audio_router)
app.include_router(models_router)
app.include_router(settings_router)
app.include_router(tasks_router)
app.include_router(cuda_router)
app.include_router(speak_router)
app.include_router(mcp_bindings_router)
app.include_router(events_router)
app.include_router(pairing_router)
# Protected — loopback callers pass through; LAN callers need a paired bearer.
protected = [Depends(require_bearer_or_loopback)]
app.include_router(profiles_router, dependencies=protected)
app.include_router(channels_router, dependencies=protected)
app.include_router(generations_router, dependencies=protected)
app.include_router(history_router, dependencies=protected)
app.include_router(transcription_router, dependencies=protected)
app.include_router(llm_router, dependencies=protected)
app.include_router(captures_router, dependencies=protected)
app.include_router(stories_router, dependencies=protected)
app.include_router(effects_router, dependencies=protected)
app.include_router(audio_router, dependencies=protected)
app.include_router(models_router, dependencies=protected)
app.include_router(settings_router, dependencies=protected)
app.include_router(tasks_router, dependencies=protected)
app.include_router(cuda_router, dependencies=protected)
app.include_router(speak_router, dependencies=protected)
app.include_router(mcp_bindings_router, dependencies=protected)
app.include_router(events_router, dependencies=protected)