Spaces:
Running
Running
| """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 | |
| 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) | |
| 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", | |
| } | |
| }, | |
| }) | |
| 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", | |
| ) | |
| 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) | |
| 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 · {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 · 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} · build {ANDROID_VERSION_CODE} · 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 — | |
| 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">“For your security, your phone is not allowed to install | |
| unknown apps from this source.”</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 — 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 — 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>""") | |