1import {christmasTarget} from './christmas-route.js?v=2026-09-23-1'; 2// ââ True Public Browse Mode (Model B: session-less) ââââââââââââââââââââââââââ 3// Pure, side-effect-free helpers so they can be unit-tested standalone (see 4// qa/selftest/public-browse.test.html) and imported by app.js without booting 5// the app. NO DOM, NO Supabase, NO auth calls here. 6// 7// Model B = logged-out visitors browse public surfaces with the anon Supabase 8// role (NO signInAnonymously). Writes/personal actions are login-gated. See 9// docs/superpowers/plans/2026-07-06-true-public-browse-mode-plan.md. 10 11// Code-side flag (never an app_config value â Codex amendment #2). It SHIPPED 12// default-OFF in PR #31 on main; this enablement branch INTENTIONALLY flips it 13// ON to activate public browse. Setting it back to `false` restores the wall. 14export const PUBLIC_BROWSE_WEB = true; 15 16export function isPublicBrowseEnabled() { 17 return PUBLIC_BROWSE_WEB === true; 18} 19 20// Boot destination. A live session always wins (full app). Otherwise the flag 21// decides: 'public' anon browse when enabled, else the legacy login 'wall'. 22export function resolveBootMode(hasSession, publicBrowseEnabled = PUBLIC_BROWSE_WEB) { 23 if (hasSession) return 'app'; 24 return publicBrowseEnabled ? 'public' : 'wall'; 25} 26 27// Feed data path. Personalized only when a real user id exists; anon visitors 28// get the 'public' path (direct posts select â never the personalized RPCs, 29// which require p_user_id). 30export function feedModeForUser(currentUser) { 31 return currentUser && currentUser.id ? 'personalized' : 'public'; 32} 33 34// Login gate for a write/personal action. Returns true when allowed (a real 35// user is present); otherwise invokes onBlocked(message) (e.g. a toast / login 36// CTA) and returns false so the caller can bail out. 37export function requireLogin(currentUser, onBlocked, message = 'Please sign in') { 38 if (currentUser && currentUser.id) return true; 39 if (typeof onBlocked === 'function') onBlocked(message); 40 return false; 41} 42 43// ââ Route auth âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 44// Hash routes that require a logged-in user. Everything not listed here (home, 45// #post, #videos, #book[/*], #video/*, #radiobox, #profile/*, path book links)
46// is a public browse surface. #messages is included for completeness though it 47// is dispatched via its own hashchange handler (messages.js also self-gates). 48export const LOGIN_REQUIRED_HASHES = Object.freeze([ 49 '#market', '#notifications', '#my-space', '#studio', '#author', '#store', '#earnings', '#bookmarks', '#messages', '#chats', 50]); 51// Prefix matches â the author book/chapter EDITORS (#author/book/<id>[/chapter/<n>]). 52export const LOGIN_REQUIRED_HASH_PREFIXES = Object.freeze(['#market/','#author/book/','#space/']); 53 54export function routeRequiresLogin(hash) { 55 if (!hash || typeof hash !== 'string') return false; 56 // Wishlist RPCs already enforce public/private Space access for anonymous readers. 57 if (christmasTarget(hash)) return false; 58 if (LOGIN_REQUIRED_HASHES.includes(hash)) return true; 59 return LOGIN_REQUIRED_HASH_PREFIXES.some(p => hash.startsWith(p)); 60} 61 62// Decide routing for a hash given the viewer: 'allow' (proceed) or 'gate' 63// (anon hitting a login-only route â caller should prompt sign-in / redirect). 64export function routeAuthDecision(hash, currentUser) { 65 if (currentUser && currentUser.id) return 'allow'; 66 return routeRequiresLogin(hash) ? 'gate' : 'allow'; 67}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.