From 6b6649236d671ca1829a04900f82a36a629013ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 02:15:23 +0000 Subject: [PATCH] Phones: sign in again when Cloudflare Access expires instead of hanging When the Access session runs out, every API call gets a cross-origin redirect to Cloudflare's login, which fetch can't follow, so the app showed "Can't reach Shell:B" forever. net.ts now checks /api/auth/me without following redirects and reloads the page so Access can sign in. The manifest link sends cookies so phones behind Access can install the app. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WMaPdTydQ3WSCNyWbHRZeg --- docs/MANUAL.md | 6 ++++++ ui/index.html | 3 ++- ui/public/sw.js | 3 ++- ui/src/net.ts | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 95b511f..a3f1eed 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -490,6 +490,12 @@ Making a share needs the owner's session (not localhost trust) and is audited. Network → **Share host** names a host that reaches this machine without Access or login in front: `share.shellb.app` (set up 2026-10-10) goes through `hub-shellb-edge`, which serves only the app shell, `/assets/*`, `/vendor/*` and `/api/share/*` with cookies stripped; links are built on it. + +**Access on phones.** When the Access session runs out, every request gets a redirect to Cloudflare's login, which a +fetch can't follow. `ui/src/net.ts` (`reloadIfEdgeLogin`) spots that (the redirect, or a 401/403 on `/api/auth/me`, +which Shell:B itself always answers with 200) and reloads the page so Access can sign in, instead of showing +"Can't reach Shell:B" forever. The manifest link carries `crossorigin="use-credentials"`, since browsers fetch it +without cookies otherwise and Access turns that away, which stops phones installing the app. The shared page renders Markdown and sandboxes HTML/SVG exactly like the owner's view, and makes no other API calls. ## Worlds diff --git a/ui/index.html b/ui/index.html index 6cc34c6..cab9c3a 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,7 +3,8 @@ - + + diff --git a/ui/public/sw.js b/ui/public/sw.js index ae328a5..1350023 100644 --- a/ui/public/sw.js +++ b/ui/public/sw.js @@ -42,6 +42,7 @@ const receiveShare = async (request) => { }; // Shown when Shell:B can't be reached and this device has never cached the app (first visit, or cleared site data). +// Its probe doesn't follow redirects: a Cloudflare Access login redirect is an answer too, and the reload signs in. const offlinePage = () => new Response(` Shell:B · offline

Can’t reach Shell:B

The Spark may be asleep, restarting or on another network. This page reloads by itself when it answers.

- + `, { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } }); self.addEventListener('fetch', (event) => { diff --git a/ui/src/net.ts b/ui/src/net.ts index b89b592..3f2cc4f 100644 --- a/ui/src/net.ts +++ b/ui/src/net.ts @@ -9,7 +9,27 @@ let down = false; let timer: number | undefined; const listeners = new Set<() => void>(); +/** + * Cloudflare Access (in front of ai.shellb.app) runs its own session. When that runs out it answers every request with + * a redirect to its login page on another site, which a fetch can't follow, so it looks exactly like the Spark is gone + * and the banner would wait forever. Only loading the page can sign in again: ask without following redirects, and if + * the answer is that redirect (or Access's own 401/403; Shell:B's /api/auth/me always answers 200), reload. + */ +const EDGE_RELOAD = 'shellb.edgeLoginAt'; +export async function reloadIfEdgeLogin() { + try { + const r = await fetch('/api/auth/me', { cache: 'no-store', redirect: 'manual' }); + if (r.type !== 'opaqueredirect' && r.status !== 401 && r.status !== 403) return; + } catch { return; } // really unreachable + let last = 0; + try { last = Number(sessionStorage.getItem(EDGE_RELOAD)) || 0; } catch { /* storage blocked */ } + if (Date.now() - last < 60_000) return; // just came back from the login and still turned away: don't loop + try { sessionStorage.setItem(EDGE_RELOAD, String(Date.now())); } catch { /* storage blocked */ } + location.reload(); +} + function set(d: boolean) { + if (d) reloadIfEdgeLogin(); if (d === down) return; down = d; window.clearInterval(timer);