Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="manifest" href="/manifest.webmanifest" />
<!-- use-credentials: browsers fetch the manifest without cookies otherwise, and Cloudflare Access turns that away, so phones can't install the app -->
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0a0d1f" />
Expand Down
3 changes: 2 additions & 1 deletion ui/public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ 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(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0a0d1f"><title>Shell:B · offline</title>
<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0a0d1f;color:#e5e7eb;font:15px/1.5 system-ui,sans-serif;padding:16px;box-sizing:border-box}
main{max-width:22rem;text-align:center}h1{font-size:18px;margin:0 0 8px}p{color:#9ca3af;margin:0 0 20px}
button{font:inherit;color:#fff;background:#7c3aed;border:0;border-radius:10px;padding:8px 18px;cursor:pointer}</style></head>
<body><main><h1>Can’t reach Shell:B</h1><p>The Spark may be asleep, restarting or on another network. This page reloads by itself when it answers.</p>
<button onclick="location.reload()">Try again</button></main>
<script>setInterval(()=>fetch('/api/auth/me',{cache:'no-store'}).then(()=>location.reload(),()=>{}),5000);addEventListener('online',()=>location.reload())</script>
<script>setInterval(()=>fetch('/api/auth/me',{cache:'no-store',redirect:'manual'}).then(()=>location.reload(),()=>{}),5000);addEventListener('online',()=>location.reload())</script>
</body></html>`, { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } });

self.addEventListener('fetch', (event) => {
Expand Down
20 changes: 20 additions & 0 deletions ui/src/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading