// App shell — auth-gated routing const COGNITO_CLIENT = '7nffjheavp985k7b4bgug45jf4'; const RAW_PAGES = { landing: { url: 'getfossyl.dev', gate: 'public' }, login: { url: 'app.getfossyl.dev/signin', gate: 'guest' }, signup: { url: 'app.getfossyl.dev/signup', gate: 'guest' }, pricing: { url: 'getfossyl.dev/pricing', gate: 'pricing' }, upload: { url: 'app.getfossyl.dev', gate: 'authed' }, report: { url: 'app.getfossyl.dev/report', gate: 'authed' }, reports: { url: 'app.getfossyl.dev/reports', gate: 'authed' }, billing: { url: 'app.getfossyl.dev/settings/billing', gate: 'authed' }, account: { url: 'app.getfossyl.dev/settings/account', gate: 'authed' }, }; function resolveRoute(target, { authed, paid }) { const def = RAW_PAGES[target] || RAW_PAGES.landing; switch (def.gate) { case 'guest': return authed ? 'upload' : target; case 'pricing': return !authed ? 'login' : (paid ? 'upload' : target); case 'authed': return authed ? target : 'login'; case 'paid': return !authed ? 'login' : (paid ? target : 'pricing'); default: return target; } } // ── Token helpers ───────────────────────────────────────────────────────────── function getCognitoIdToken() { const last = localStorage.getItem(`CognitoIdentityServiceProvider.${COGNITO_CLIENT}.LastAuthUser`); if (!last) return null; return localStorage.getItem(`CognitoIdentityServiceProvider.${COGNITO_CLIENT}.${last}.idToken`); } function isTokenFresh(token) { if (!token) return false; try { const { exp } = JSON.parse(atob(token.split('.')[1])); return exp * 1000 > Date.now() + 30_000; // 30s buffer } catch { return false; } } // Global getter used by all API helpers across pages window.getIdToken = () => getCognitoIdToken() || ''; // Single source of truth for the signatures-only payload description. // Must enumerate exactly the field types emitted by the extractor allow-list // in fossyl/analysis/intent_inferrer.py. The copy-drift validator in graph.jsx // asserts this string contains all required tokens and no forbidden ones. window.FOSSYL_SIGNATURES_PAYLOAD_DESC = "## Zope header directives (Parameters, Returns, Bind), " + "import and from-import module names, " + "function and class signatures with parameter names and type annotations " + "(no default values or bodies), " + "and METAL macro/slot names"; // ── Mount ───────────────────────────────────────────────────────────────────── function Mount() { const [authed, setAuthed] = useState(() => localStorage.getItem('fossyl_authed') === '1'); const [paid, setPaid] = useState(() => localStorage.getItem('fossyl_paid') === '1'); const [hasRun, setHasRun] = useState(() => localStorage.getItem('fossyl_run') === '1'); const [plan, setPlan] = useState(() => localStorage.getItem('fossyl_plan') || 'team'); const [role, setRole] = useState(() => localStorage.getItem('fossyl_role') || 'Owner'); const [page, setPage] = useState(() => localStorage.getItem('fossyl_page') || 'landing'); const [sessionExpired, setSessionExpired] = useState(false); const [ghAuthError, setGhAuthError] = useState(null); const [pendingInvite, setPendingInvite] = useState(null); const [acceptingInvite, setAcceptingInvite] = useState(false); const [pendingTransfer, setPendingTransfer] = useState(null); const [acceptingTransfer,setAcceptingTransfer]= useState(false); // ── Stripe Checkout redirect ─────────────────────────────────────────────── useEffect(() => { const search = window.location.search; if (!search) return; const params = new URLSearchParams(search); if (params.get('checkout_cancel')) { window.history.replaceState(null, '', '/'); setPage('billing'); return; } if (params.get('checkout_success') && params.get('session_id')) { const sessionId = params.get('session_id'); window.history.replaceState(null, '', '/'); const token = localStorage.getItem( `CognitoIdentityServiceProvider.${COGNITO_CLIENT}.` + `${localStorage.getItem(`CognitoIdentityServiceProvider.${COGNITO_CLIENT}.LastAuthUser`)}.idToken` ); fetch( `https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/billing/verify?session_id=${encodeURIComponent(sessionId)}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} } ) .then(r => r.json()) .then(data => { if (data.status === 'active') { setAuthed(true); setPaid(true); setPlan(data.plan || 'team'); setSessionExpired(false); setPage('upload'); } else { setPage('billing'); } }) .catch(() => setPage('billing')); } }, []); // eslint-disable-line // ── GitHub OAuth callback ────────────────────────────────────────────────── useEffect(() => { const hash = window.location.hash.slice(1); if (!hash) return; const params = new URLSearchParams(hash); if (params.get('gh_error')) { window.history.replaceState(null, '', '/'); setGhAuthError(decodeURIComponent(params.get('gh_error'))); setPage('login'); return; } const idToken = params.get('gh_id_token'); const accessToken = params.get('gh_access_token'); const refreshToken = params.get('gh_refresh_token'); const email = params.get('gh_email'); const expiry = params.get('gh_expiry'); if (!idToken || !email) return; // Store in Cognito SDK format so getCognitoIdToken() picks it up const prefix = `CognitoIdentityServiceProvider.${COGNITO_CLIENT}`; localStorage.setItem(`${prefix}.LastAuthUser`, email); localStorage.setItem(`${prefix}.${email}.idToken`, idToken); localStorage.setItem(`${prefix}.${email}.accessToken`, accessToken); localStorage.setItem(`${prefix}.${email}.refreshToken`, refreshToken); // Also store in fossyl_ keys for any consumers that use them localStorage.setItem('fossyl_id_token', idToken); localStorage.setItem('fossyl_access_token', accessToken); localStorage.setItem('fossyl_refresh_token', refreshToken); localStorage.setItem('fossyl_email', email); if (expiry) localStorage.setItem('fossyl_token_expiry', expiry); window.history.replaceState(null, '', '/'); setAuthed(true); setSessionExpired(false); // subscription check useEffect will set paid/plan once authed flips to true }, []); // eslint-disable-line // ── Token check on mount and tab focus ──────────────────────────────────── useEffect(() => { function checkToken() { const token = getCognitoIdToken(); if (isTokenFresh(token)) { // Valid token — ensure state is consistent; subscription check handles paid/plan if (!authed) { setAuthed(true); } setSessionExpired(false); return; } // No valid token if (authed) { // Was logged in but token expired — clear state and show expiry notice setAuthed(false); setPaid(false); setSessionExpired(true); } } // Only run the check if we think we're logged in // (avoids false positives on landing page before any login) if (localStorage.getItem('fossyl_authed') === '1') { checkToken(); } // Guard the focus handler: after an explicit sign-out fossyl_authed is '0', // so we must not re-authenticate even if a stale Cognito token is still readable. function checkTokenIfAuthed() { if (localStorage.getItem('fossyl_authed') === '1') checkToken(); } window.addEventListener('focus', checkTokenIfAuthed); return () => window.removeEventListener('focus', checkTokenIfAuthed); }, []); // eslint-disable-line // ── Subscription + role check — runs on login and on mount if already authed ─ useEffect(() => { if (!authed) return; const token = getCognitoIdToken(); if (!isTokenFresh(token)) return; // stale localStorage authed flag — skip, token check will correct it const h = { Authorization: `Bearer ${token}` }; const base = 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod'; Promise.all([ fetch(`${base}/billing/subscription`, { headers: h }).then(r => r.json()).catch(() => ({})), fetch(`${base}/account/profile`, { headers: h }).then(r => r.json()).catch(() => ({})), ]).then(([sub, prof]) => { setPaid(!!sub.paid); if (sub.plan) setPlan(sub.plan); if (prof.role) setRole(prof.role); }); }, [authed]); // eslint-disable-line // ── Pending invitation + transfer check ────────────────────────────────── useEffect(() => { if (!authed) return; const token = getCognitoIdToken(); if (!isTokenFresh(token)) return; // stale localStorage authed flag — skip const base = 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod'; const h = { Authorization: `Bearer ${token}` }; fetch(`${base}/account/invite`, { headers: h }) .then(r => r.json()) .then(data => { if (data.invite) setPendingInvite(data.invite); }) .catch(() => {}); fetch(`${base}/account/workspace/transfer`, { headers: h }) .then(r => r.json()) .then(data => { if (data.transfer) setPendingTransfer(data.transfer); }) .catch(() => {}); }, [authed]); // eslint-disable-line // ── Persist ─────────────────────────────────────────────────────────────── useEffect(() => { localStorage.setItem('fossyl_authed', authed ? '1' : '0'); }, [authed]); useEffect(() => { localStorage.setItem('fossyl_paid', paid ? '1' : '0'); }, [paid]); useEffect(() => { localStorage.setItem('fossyl_run', hasRun ? '1' : '0'); }, [hasRun]); useEffect(() => { localStorage.setItem('fossyl_plan', plan); }, [plan]); useEffect(() => { localStorage.setItem('fossyl_role', role); }, [role]); useEffect(() => { localStorage.setItem('fossyl_page', page); }, [page]); // ── Routing ─────────────────────────────────────────────────────────────── useEffect(() => { const resolved = resolveRoute(page, { authed, paid }); if (resolved !== page) setPage(resolved); }, [page, authed, paid]); const nav = useMemo(() => ({ current: page, go: (id) => { const resolved = resolveRoute(id, { authed, paid }); setPage(resolved); window.scrollTo(0, 0); }, }), [page, authed, paid]); const auth = useMemo(() => ({ authed, paid, hasRun, plan, role, signIn: () => { setAuthed(true); setSessionExpired(false); }, signUp: () => { setAuthed(true); setSessionExpired(false); }, signOut: () => { if (window.clearTokens) window.clearTokens(); setAuthed(false); setPaid(false); setHasRun(false); setRole('Owner'); setSessionExpired(false); }, setPaid, setHasRun, setPlan, setRole, }), [authed, paid, hasRun, plan, role]); const handleInviteAccept = async () => { setAcceptingInvite(true); try { const token = getCognitoIdToken(); const res = await fetch( 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/account/invite', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'accept' }) } ); const data = await res.json(); if (data.accepted) { setRole(data.role || 'Member'); setPendingInvite(null); // Re-check subscription since workspace/plan may have changed const sub = await fetch( 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/billing/subscription', { headers: { Authorization: `Bearer ${token}` } } ).then(r => r.json()).catch(() => ({})); setPaid(!!sub.paid); if (sub.plan) setPlan(sub.plan); } } catch {} setAcceptingInvite(false); }; const handleInviteDecline = async () => { try { const token = getCognitoIdToken(); await fetch( 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/account/invite', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'decline' }) } ); } catch {} setPendingInvite(null); }; const handleTransferAccept = async () => { setAcceptingTransfer(true); try { const token = getCognitoIdToken(); const res = await fetch( 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/account/workspace/transfer/accept', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: '{}' } ); const data = await res.json(); if (data.accepted) { setRole('Owner'); setPendingTransfer(null); } } catch {} setAcceptingTransfer(false); }; const handleTransferDecline = async () => { try { const token = getCognitoIdToken(); await fetch( 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod/account/workspace/transfer/decline', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: '{}' } ); } catch {} setPendingTransfer(null); }; return ( {pendingInvite && ( )} {pendingTransfer && ( )} {/* GitHub auth error banner */} {ghAuthError && (
GitHub sign-in failed — {ghAuthError}
)} {/* Session expiry banner — shown on any authed page when token has expired */} {sessionExpired && page !== 'login' && (
Session expired — please sign in again to continue.
)} {page === 'landing' && } {page === 'login' && } {page === 'signup' && } {page === 'pricing' && } {page === 'upload' && } {page === 'report' && } {page === 'reports' && } {page === 'billing' && } {page === 'account' && }
); } const rootEl = document.getElementById('root'); rootEl.removeAttribute('data-babel-unloaded'); ReactDOM.createRoot(rootEl).render(); window.__graph = window.__graph || 'force';