jarvis-cloud / backend /routes /distribution_routes.py
Jarvis2345's picture
deploy(S4): Blender headless pipeline + WebAR client + backend fixes
f694ed4 verified
Raw
History Blame Contribute Delete
13.5 kB
"""Distribution and update delivery.
Part 35 established, with live evidence, that a subscriber currently has no way to install the
APK and no way to learn a new version exists:
* the advertised APK URL returned 404 and no .apk existed anywhere on the Space
* family_device_router was never mounted, so the only enrollment endpoint was unreachable
* the exe's Tauri updater pointed at api.starkindustries.com, which is NXDOMAIN
* the APK had no update mechanism at all
Those were findings. This module is the fix. Every route here is deliberately unauthenticated
or lightly authenticated, because a person who has just paid and is standing at a download page
does not yet have the app that holds their token -- gating the download itself behind the app
you are trying to download is a bootstrap the product cannot survive. Entitlement is enforced
where it belongs (on the features, server-side, per the standing plan-gating rule), not on the
installer bytes.
"""
from __future__ import annotations
import hashlib
import logging
import os
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from backend.version import (
ANDROID_VERSION_CODE,
EXE_TARGET,
VERSION,
manifest,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/distribution", tags=["distribution"])
_ROOT = Path(__file__).resolve().parents[2]
# Where a real signed artifact may actually sit. Checked in order; the first that exists wins.
# OMEGA_APK_PATH lets the operator drop the artifact anywhere without a code change.
_APK_CANDIDATES = [
os.environ.get("OMEGA_APK_PATH", ""),
str(_ROOT / "dist" / "app-release.apk"),
str(_ROOT / "phone" / "jarvis-mobile-guardian" / "app" / "build" / "outputs" / "apk" / "release" / "app-release.apk"),
]
_EXE_CANDIDATES = [
os.environ.get("OMEGA_EXE_PATH", ""),
str(_ROOT / "dist" / "JARVIS-OS-setup.exe"),
str(_ROOT / "src-tauri" / "target" / "release" / "bundle" / "msi" / f"JARVIS OS_{VERSION}_x64_en-US.msi"),
]
def _first_existing(candidates: list[str]) -> Path | None:
for c in candidates:
if not c:
continue
p = Path(c)
if p.is_file() and p.stat().st_size > 0:
return p
return None
def _sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def _base_url(request: Request) -> str:
"""Absolute base URL, honouring the proxy that actually terminates TLS.
The Space runs behind HF's reverse proxy, so `request.base_url` sees the *internal* hop and
reports http://. That made the live manifest advertise `http://.../distribution/android` from
a page served over HTTPS -- a mixed-content download link, which browsers block outright and
which no updater should be asked to follow. Verified live: the deployed manifest returned an
http:// URL until this read X-Forwarded-Proto.
"""
base = str(request.base_url).rstrip("/")
proto = request.headers.get("x-forwarded-proto", "").split(",")[0].strip()
if proto in ("http", "https") and base.startswith(("http://", "https://")):
scheme, _, rest = base.partition("://")
if scheme != proto:
base = f"{proto}://{rest}"
return base
@router.get("/manifest")
async def distribution_manifest(request: Request):
"""The one payload every update check on every platform reads.
The exe's updater, the APK's in-app check, and any operator tooling all read this, so the
six-surfaces-five-versions drift Part 35 found cannot recur: there is one source
(backend/version.py) and one route.
"""
data = manifest(_base_url(request))
# Report what is genuinely downloadable right now rather than advertising a URL that 404s,
# which is precisely the failure this module exists to correct.
apk = _first_existing(_APK_CANDIDATES)
data["android"]["available"] = apk is not None
if apk is not None:
data["android"]["sizeBytes"] = apk.stat().st_size
data["android"]["sha256"] = _sha256(apk)
else:
data["android"]["notes"] += " -- artifact not present on this host yet"
exe = _first_existing(_EXE_CANDIDATES)
data["exe"]["available"] = exe is not None
if exe is not None:
data["exe"]["sizeBytes"] = exe.stat().st_size
return JSONResponse(data)
@router.get("/updates/{target}/{current_version}")
async def tauri_update_check(target: str, current_version: str, request: Request):
"""Tauri updater endpoint.
Tauri's contract: HTTP 204 means "you are current", HTTP 200 with a body means "here is the
update". Returning 204 is the correct, non-noisy answer for an up-to-date client -- the old
config pointed at a domain that does not resolve, so every check failed at DNS and the app
silently believed it had checked.
"""
if target != EXE_TARGET:
# An unknown target is not an error the user should see; there is simply nothing for it.
return Response(status_code=204)
if current_version == VERSION:
return Response(status_code=204)
exe = _first_existing(_EXE_CANDIDATES)
if exe is None:
# Honest: we know a newer version exists but have nothing to hand out on this host.
return Response(status_code=204)
# src-tauri/tauri.conf.json sets `updater.pubkey`, so Tauri v1 verifies a minisign signature
# over the artifact and REFUSES anything that does not match. This endpoint previously sent
# `"signature": ""`, which means the client downloads the whole installer and then rejects it
# -- a failure the user experiences as an update that silently never applies.
#
# Offering an update we know will be refused is worse than offering none, so the honest answer
# while no signature exists is "you are current". OMEGA_EXE_SIGNATURE carries the output of
# `tauri signer sign` for the artifact being served.
signature = os.environ.get("OMEGA_EXE_SIGNATURE", "").strip()
if not signature:
return Response(status_code=204)
return JSONResponse({
"version": VERSION,
"notes": f"JARVIS OS {VERSION}",
"pub_date": None,
"platforms": {
EXE_TARGET: {
"signature": signature,
"url": f"{_base_url(request)}/distribution/windows",
}
},
})
@router.get("/android")
async def download_android(request: Request):
"""Serve the actual signed release APK."""
apk = _first_existing(_APK_CANDIDATES)
if apk is None:
# 503, not 404. A 404 says "there is no such thing"; the truth is "this exists but is
# not staged on this host", which is a different problem with a different fix.
return JSONResponse(
status_code=503,
content={
"error": "apk_not_staged",
"message": (
"The Guardian APK is not present on this host. Set OMEGA_APK_PATH, or "
"publish the signed artifact and set OMEGA_APK_URL."
),
"expectedVersion": VERSION,
"expectedVersionCode": ANDROID_VERSION_CODE,
},
)
return FileResponse(
apk,
media_type="application/vnd.android.package-archive",
filename=f"jarvis-guardian-{VERSION}.apk",
)
@router.get("/windows")
async def download_windows(request: Request):
exe = _first_existing(_EXE_CANDIDATES)
if exe is None:
return JSONResponse(
status_code=503,
content={
"error": "installer_not_staged",
"message": "The Windows installer is not present on this host.",
"expectedVersion": VERSION,
},
)
return FileResponse(exe, media_type="application/octet-stream", filename=exe.name)
@router.get("/install", response_class=HTMLResponse)
async def install_page(request: Request):
"""The page a subscriber is actually sent to.
Android blocks installs from outside Play by default, and that prompt is unavoidable
outside the Play Store. The honest thing is to tell the user it is coming, in the exact
words their phone will use, *before* it appears -- an unexplained system security prompt
mid-install is where sideload flows lose people. Same principle as explaining the camera
permission before the WebAR gate triggers it.
"""
base = _base_url(request)
apk = _first_existing(_APK_CANDIDATES)
staged = apk is not None
size_mb = f"{apk.stat().st_size / (1024 * 1024):.0f} MB" if staged else "—"
cta = (
f'<a class="cta" href="{base}/distribution/android">Download Guardian &middot; {size_mb}</a>'
if staged
else '<p class="unavailable">The download is not available on this server yet. '
'Nothing is wrong with your account.</p>'
)
return HTMLResponse(f"""<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Install JARVIS Guardian</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Orbitron:wght@600&family=Barlow+Condensed:wght@600&family=DM+Sans:opsz,wght@9..40,300..700&display=swap">
<style>
:root {{
--void:#0A0A0F; --surface:#12121A; --accent:#00D4FF; --text:#fff;
--muted:rgba(255,255,255,.62); --amber:#FF9F0A; --border:rgba(255,255,255,.09);
}}
* {{ box-sizing:border-box }}
body {{ margin:0; padding:28px 20px 48px; background:
radial-gradient(120% 60% at 50% -10%, rgba(0,212,255,.13), transparent 60%), var(--void);
color:var(--text); font-family:'DM Sans',system-ui,sans-serif; line-height:1.55;
display:flex; justify-content:center; }}
.wrap {{ width:100%; max-width:520px }}
.eyebrow {{ font-family:'Barlow Condensed',sans-serif; letter-spacing:.22em; font-size:13px;
color:var(--accent); margin:0 0 4px }}
h1 {{ font-family:'Orbitron',sans-serif; font-size:26px; margin:0; letter-spacing:.01em }}
.rail {{ width:46px; height:2px; background:var(--accent); box-shadow:0 0 10px rgba(0,212,255,.5); margin:9px 0 20px }}
.cta {{ display:block; text-align:center; text-decoration:none; padding:16px;
background:var(--accent); color:var(--void); border-radius:14px;
font-family:'Barlow Condensed',sans-serif; font-size:19px; font-weight:600;
letter-spacing:.1em; margin:22px 0 10px }}
.unavailable {{ padding:14px; border-radius:12px; background:rgba(255,159,10,.08);
border:1px solid rgba(255,159,10,.3); color:var(--amber); font-size:14px }}
.meta {{ text-align:center; color:var(--muted); font-size:12px; margin:0 0 26px }}
.card {{ background:var(--surface); border:1px solid var(--border); border-radius:16px;
padding:16px 18px; margin:0 0 14px }}
.card h2 {{ font-family:'Barlow Condensed',sans-serif; font-size:15px; letter-spacing:.16em;
margin:0 0 10px; color:var(--accent) }}
ol {{ margin:0; padding-left:20px }} li {{ margin:0 0 9px; font-size:14.5px }}
.quote {{ display:block; margin:6px 0 0; padding:9px 11px; border-radius:9px;
background:rgba(255,255,255,.05); border-left:2px solid var(--amber);
color:var(--muted); font-size:13px }}
footer {{ text-align:center; color:rgba(255,255,255,.38); font-size:11.5px; margin-top:22px }}
</style></head>
<body><div class="wrap">
<p class="eyebrow">JARVIS &middot; OMEGA</p>
<h1>Install Guardian</h1>
<div class="rail"></div>
<p style="color:var(--muted);margin:0">The Guardian companion app pairs with JARVIS on your
PC. It is distributed directly by us rather than through the Play Store, so there is one
extra confirmation step your phone will ask for.</p>
{cta}
<p class="meta">Version {VERSION} &middot; build {ANDROID_VERSION_CODE} &middot; Android 8.0 or newer</p>
<div class="card">
<h2>WHAT YOUR PHONE WILL ASK</h2>
<ol>
<li>Tap <b>Download</b>. Chrome may warn that this file type can harm your device &mdash;
that warning appears for every APK, including ones from us. Tap <b>Download anyway</b>.</li>
<li>Open the downloaded file. Android will say something close to:
<span class="quote">&ldquo;For your security, your phone is not allowed to install
unknown apps from this source.&rdquo;</span></li>
<li>Tap <b>Settings</b> on that prompt, turn on <b>Allow from this source</b>, then press
back. You only ever do this once.</li>
<li>Tap <b>Install</b>, then <b>Open</b>. Guardian will show a pairing code &mdash; enter it
on your PC and you are done.</li>
</ol>
</div>
<div class="card">
<h2>IS THIS SAFE</h2>
<p style="margin:0;font-size:14.5px;color:var(--muted)">Yes &mdash; the prompt is Android
protecting you from apps it has not personally reviewed, not a warning about this app
specifically. The download is served over HTTPS and the APK is signed with our release key,
so your phone will refuse any future update that is not signed by the same key.</p>
</div>
<footer>Trouble installing? Reply to your welcome email and we will help.</footer>
</div></body></html>""")