--lan first load: redirect commits the key cookie; static goes public

The phone's first visit 403'd its own subresources: Safari's preload
scanner fetches /static/* before the document response's Set-Cookie is
committed, and favicon/apple-touch-icon probes are cookie-less system
fetches. A ?k= visit now answers 303-with-cookie to the same path —
the cookie is committed before any document loads, and the key is
scrubbed from the phone's address bar and history. /static/* and the
icon probe paths are exempt from the key: they're the app's own
css/js/artwork, no user data (shelf photos stay gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 16:31:57 -04:00
co-authored by Claude Fable 5
parent 95fd18ca5c
commit 344a930a8a
3 changed files with 38 additions and 13 deletions
+28 -9
View File
@@ -330,11 +330,24 @@ def create_app(
origin_host = urlsplit(origin).hostname if origin else None
client_ip = request.client.host if request.client else ""
loopback = client_ip.startswith("127.") or client_ip == "::1"
# /static/* is the app's own css/js/artwork — no user data. Safari's
# preload scanner fetches them before the document's Set-Cookie is
# committed, and browsers probe favicons cookie-less; gating them
# would break the first paint for nothing.
public = request.url.path.startswith("/static/") or request.url.path in (
"/favicon.ico",
"/apple-touch-icon.png",
"/apple-touch-icon-precomposed.png",
)
# Same-machine browsing stays keyless under --lan: a network peer
# cannot arrive with a loopback CLIENT address, and these requests
# fall through to the localhost guard below, which still blocks
# rebinding and cross-origin pages by their foreign Host/Origin.
if lan_token is not None and not (loopback and host in ALLOWED_HOSTS):
if (
lan_token is not None
and not (loopback and host in ALLOWED_HOSTS)
and not public
):
# --lan has no login, so EVERY request — reads included: shelf
# photos and pipeline state are private — needs the per-run
# key from the printed URL; a cookie carries it afterwards.
@@ -355,20 +368,26 @@ def create_app(
},
status_code=403,
)
if request.method in ("GET", "HEAD") and request.query_params.get("k"):
# the key arrived in the typed URL: commit it to a cookie
# via redirect BEFORE the document loads (the preload
# scanner races Set-Cookie on a streaming document), and
# scrub the key from the address bar and history
response = Response(
status_code=303,
headers={"Location": request.url.path or "/"},
)
response.set_cookie(
LAN_COOKIE, lan_token, httponly=True, samesite="lax"
)
return response
if request.method not in ("GET", "HEAD", "OPTIONS") and (
origin_host is not None and origin_host != host
):
return JSONResponse(
{"detail": "cross-origin request refused"}, status_code=403
)
response = await call_next(request)
if request.query_params.get("k"):
# the key came in the typed URL: hand it to the browser so
# navigation and fetches keep working without it
response.set_cookie(
LAN_COOKIE, lan_token, httponly=True, samesite="lax"
)
return response
return await call_next(request)
if request.method not in ("GET", "HEAD", "OPTIONS") and (
host not in ALLOWED_HOSTS
or (origin_host is not None and origin_host not in ALLOWED_HOSTS)