Harden website auth shell routing

This commit is contained in:
axiomlogicnexus 2026-06-24 08:12:56 +00:00
parent c8cb1bfb8d
commit e13c4bc4c5
8 changed files with 331 additions and 5 deletions

View file

@ -258,6 +258,44 @@ Latest same-family public-manual follow-up on `2026-06-24`:
- embedded simulator browser shell
- native Unreal desktop runtime
- optional full-browser simulator branch
Latest auth-shell hardening follow-up later on `2026-06-24`:
- the same public/protected website lane now also carries the sibling-site
auth-shell posture more faithfully instead of only reusing auth APIs and
route pages
- HyperTwist now has its own first-party auth-shell backdrop ownership for:
- `/login`
- `/register`
- `/auth/*`
- that backdrop is now present through both:
- a runtime route-sync layer inside the React app
- a first-paint HTML fallback in `website/index.html`
- the `SuperTokensWrapper` posture is now route-aware rather than global:
- anonymous brochure routes no longer pay for the full auth shell merely
because auth is configured
- protected app routes, auth routes, and stored-session continuity still do
- this keeps HyperTwist closer to the intended FamiliarOS/ScriptoriumAI
structure while remaining truthful to HyperTwist's desktop-first product
stance
- focused auth/bootstrap validation stayed green under:
- `npm --prefix website test -- --run src/__tests__/App.bootstrap.test.tsx src/__tests__/auth-shell-backdrop.test.ts src/__tests__/public-auth-pages.test.tsx src/__tests__/route-shells.test.tsx`
- `4` test files passed
- `21` tests passed
- the broader owned web-surface umbrella also stayed green again under:
- `scripts/run-hypertwist-web-surface-validation.sh`
- focused website route/auth/release suite: `12` files, `62` tests passed
- website/server suite: `10` files, `36` tests passed
- website plus `Content/Browser` production audits: `found 0 vulnerabilities`
- website/server retained only the already-documented upstream
`supertokens-node -> nodemailer` residual
- current truthful product reading after this follow-up:
- the public website and protected dashboard still stay complementary to the
desktop runtime rather than pretending to replace it
- the shared auth lineage is now not only conceptual but also structural at
the bootstrap/shell layer
- the next public/web packets can focus on higher-order product polish or
deployment truth rather than missing auth-shell parity
- the public `/changelog` lane now also carries an explicit release-rollout
checklist so release notes remain tied to package proof, protected download
posture, and notices or corresponding-source follow-through instead of

View file

@ -7,6 +7,8 @@ First-party `hypertwist.app` surface for HyperTwist:
- browser-facing operator/account dashboard
- protected browser-access, account, and notices routes backed by live auth-health and release-manifest authority
- shared SuperTokens auth posture reused from the FamiliarOS and ScriptoriumAI website lane
- route-aware SuperTokens wrapping so public brochure routes do not pay for auth chrome unnecessarily while `/app`, `/login`, `/register`, `/auth/*`, and stored-session continuity still do
- FamiliarOS-style first-party auth-shell backdrop for login/register/auth callback posture, including first-paint HTML fallback plus route-synced runtime ownership
- desktop download posture and desktop-link handshake endpoints
- server-backed release-manifest authority shared by public and protected download surfaces
- Paddle-ready pricing/check-out wiring
@ -61,6 +63,10 @@ manual:
- the protected app shell now also carries richer operator-facing browser
boundary, account, entitlement, and notices guidance instead of treating
those routes as thin placeholders beside the main dashboard
- the auth shell now also behaves more like the sibling first-party sites at
the route/bootstrap layer: auth-specific chrome appears on login/register
posture without wrapping every anonymous public route in the same session
shell
## Local development
@ -115,6 +121,35 @@ and supports `--strict-auth-server-audit` when that residual should block.
It now also checks that the website-facing Windows packaged-validation summary
is fresh against the checked-in authoritative higher-dimensional package report.
Latest auth-shell hardening follow-up on `2026-06-24`:
- the website now carries a first-party HyperTwist auth-shell backdrop module
and route sync layer derived from the FamiliarOS/ScriptoriumAI auth shell
posture, but adapted to HyperTwist truth:
- `/login`
- `/register`
- `/auth/*`
- `App.tsx` no longer wraps the entire website in `SuperTokensWrapper` just
because auth is configured; it now wraps only:
- `/app*`
- `/login*`
- `/register*`
- `/auth/*`
- or public routes when a stored local platform session already exists
- `website/index.html` now also carries a first-paint auth-backdrop fallback
so auth-shell chrome is present before React route effects settle
- focused website auth/bootstrap validation stayed green under:
- `npm --prefix website test -- --run src/__tests__/App.bootstrap.test.tsx src/__tests__/auth-shell-backdrop.test.ts src/__tests__/public-auth-pages.test.tsx src/__tests__/route-shells.test.tsx`
- `4` test files passed
- `21` tests passed
- the broader owned web-surface umbrella then stayed green again under:
- `scripts/run-hypertwist-web-surface-validation.sh`
- focused website route/auth/release suite: `12` files, `62` tests passed
- website/server suite: `10` files, `36` tests passed
- website and `Content/Browser` production audits: `found 0 vulnerabilities`
- website/server retained only the already-documented upstream
`supertokens-node -> nodemailer` residual
Current dependency-health truth from the `2026-06-23` hardening pass:
- `website/` production audit is clean

View file

@ -36,6 +36,54 @@
</head>
<body>
<div id="root"></div>
<script>
(function () {
const BACKDROP_ID = 'hypertwist-auth-shell-backdrop';
function isAuthRoute(pathname) {
return pathname === '/login'
|| pathname.startsWith('/login/')
|| pathname === '/register'
|| pathname.startsWith('/register/')
|| pathname.startsWith('/auth/');
}
function syncAuthChrome() {
const shouldRender = isAuthRoute(window.location.pathname);
const existingBackdrop = document.getElementById(BACKDROP_ID);
if (!shouldRender) {
if (existingBackdrop) existingBackdrop.remove();
return;
}
if (!existingBackdrop) {
const backdrop = document.createElement('div');
backdrop.id = BACKDROP_ID;
backdrop.setAttribute('data-testid', 'supertokens-auth-backdrop');
backdrop.setAttribute('aria-hidden', 'true');
backdrop.style.position = 'fixed';
backdrop.style.inset = '0';
backdrop.style.zIndex = '0';
backdrop.style.pointerEvents = 'none';
backdrop.style.background = 'radial-gradient(circle at top, rgba(216,203,175,0.12), transparent 40%), linear-gradient(180deg, rgba(18,20,24,0.97), rgba(10,12,17,0.99))';
document.body.appendChild(backdrop);
}
}
function startAuthChromeObserver() {
syncAuthChrome();
window.addEventListener('popstate', syncAuthChrome);
window.addEventListener('hashchange', syncAuthChrome);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startAuthChromeObserver, { once: true });
} else {
startAuthChromeObserver();
}
})();
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,9 +1,10 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import { BrowserRouter, useLocation } from 'react-router-dom'
import { SuperTokensWrapper } from 'supertokens-auth-react'
import { PlatformAuthProvider } from './auth/platform-auth'
import { isSuperTokensConfigured } from './auth/supertokens-client'
import { AppRouteTree } from './router/AppRouteTree'
import { AuthShellBackdropSync } from './router/AuthShellBackdropSync'
import { ROUTER_FUTURE_FLAGS } from './router/router-future'
import './styles/global.css'
@ -17,11 +18,36 @@ const queryClient = new QueryClient({
},
})
function routeRequiresAuthShell(pathname: string) {
return pathname.startsWith('/app')
|| pathname === '/login'
|| pathname.startsWith('/login/')
|| pathname === '/register'
|| pathname.startsWith('/register/')
|| pathname.startsWith('/auth/')
}
function hasStoredPlatformUser() {
if (typeof window === 'undefined') return false
try {
return Boolean(window.localStorage.getItem('hypertwist.platform.user.v1'))
} catch {
return false
}
}
function MaybeSuperTokensWrapper({ children }: { children: React.ReactNode }) {
const location = useLocation()
if (!isSuperTokensConfigured()) {
return <>{children}</>
}
const needsWrapper = routeRequiresAuthShell(location.pathname) || hasStoredPlatformUser()
if (!needsWrapper) {
return <>{children}</>
}
return <SuperTokensWrapper>{children}</SuperTokensWrapper>
}
@ -29,6 +55,7 @@ export default function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter future={ROUTER_FUTURE_FLAGS}>
<AuthShellBackdropSync />
<MaybeSuperTokensWrapper>
<PlatformAuthProvider>
<AppRouteTree />

View file

@ -9,9 +9,21 @@ vi.mock('../auth/platform-auth', () => ({
usePlatformAuth: () => mockUsePlatformAuth(),
}))
vi.mock('../auth/supertokens-client', () => ({
isSuperTokensConfigured: () => mockIsSuperTokensConfigured(),
}))
vi.mock('../auth/supertokens-client', async (importOriginal) => {
const actual = await importOriginal<typeof import('../auth/supertokens-client')>()
return {
...actual,
isSuperTokensConfigured: () => mockIsSuperTokensConfigured(),
isGoogleOAuthEnabled: () => false,
isGitHubOAuthEnabled: () => false,
isOrcidOAuthEnabled: () => false,
getSuperTokensAuthRuntimeValidation: () => ({
ready: true,
missing: [],
warnings: [],
}),
}
})
vi.mock('supertokens-auth-react', () => ({
SuperTokensWrapper: ({ children }: { children: React.ReactNode }) => (
@ -24,6 +36,7 @@ import App from '../App'
describe('App bootstrap', () => {
beforeEach(() => {
cleanup()
window.localStorage.clear()
mockUsePlatformAuth.mockReset()
mockIsSuperTokensConfigured.mockReset()
mockIsSuperTokensConfigured.mockReturnValue(false)
@ -53,7 +66,7 @@ describe('App bootstrap', () => {
expect(screen.queryByTestId('supertokens-wrapper')).toBeNull()
})
it('wraps the app in SuperTokens when configured and still renders the real public route', async () => {
it('does not wrap ordinary public routes in SuperTokens just because auth is configured', async () => {
mockIsSuperTokensConfigured.mockReturnValue(true)
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: false,
@ -70,6 +83,63 @@ describe('App bootstrap', () => {
render(<App />)
expect(await screen.findByText('Pricing that matches the actual delivery model.')).toBeTruthy()
expect(screen.queryByTestId('supertokens-wrapper')).toBeNull()
expect(document.querySelector('[data-testid="supertokens-auth-backdrop"]')).toBeNull()
})
it('wraps auth-shell routes in SuperTokens and syncs the auth backdrop', async () => {
mockIsSuperTokensConfigured.mockReturnValue(true)
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: false,
isLoading: false,
login: vi.fn(),
register: vi.fn(),
logout: vi.fn(),
toggleColorMode: vi.fn(),
colorMode: 'dark',
superTokensConfigured: true,
user: null,
})
window.history.replaceState({}, '', '/login')
render(<App />)
expect(await screen.findByText('Log in to HyperTwist')).toBeTruthy()
expect(screen.getByTestId('supertokens-wrapper')).toBeTruthy()
expect(document.querySelector('[data-testid="supertokens-auth-backdrop"]')).not.toBeNull()
})
it('keeps the SuperTokens wrapper active on public routes when a local platform session is already stored', async () => {
mockIsSuperTokensConfigured.mockReturnValue(true)
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
login: vi.fn(),
register: vi.fn(),
logout: vi.fn(),
toggleColorMode: vi.fn(),
colorMode: 'dark',
superTokensConfigured: true,
user: {
id: 'operator-1',
name: 'Operator',
email: 'operator@hypertwist.app',
authMethod: 'supertokens',
plan: 'operator',
},
})
window.localStorage.setItem('hypertwist.platform.user.v1', JSON.stringify({
id: 'operator-1',
name: 'Operator',
email: 'operator@hypertwist.app',
authMethod: 'supertokens',
plan: 'operator',
}))
window.history.replaceState({}, '', '/pricing')
render(<App />)
expect(await screen.findByText('Pricing that matches the actual delivery model.')).toBeTruthy()
expect(screen.getByTestId('supertokens-wrapper')).toBeTruthy()
})

View file

@ -0,0 +1,49 @@
import { beforeEach, describe, expect, it } from 'vitest'
import {
AUTH_SHELL_BACKDROP_ID,
ensureAuthShellBackdrop,
isAuthShellRoute,
syncAuthShellBackdrop,
} from '../auth/auth-shell-backdrop'
describe('auth-shell-backdrop', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>'
})
it('recognizes login, register, and auth callback routes', () => {
expect(isAuthShellRoute('/login')).toBe(true)
expect(isAuthShellRoute('/login/reset')).toBe(true)
expect(isAuthShellRoute('/register')).toBe(true)
expect(isAuthShellRoute('/register/invite')).toBe(true)
expect(isAuthShellRoute('/auth/callback')).toBe(true)
expect(isAuthShellRoute('/app')).toBe(false)
expect(isAuthShellRoute('/pricing')).toBe(false)
})
it('creates the auth backdrop with the expected styling', () => {
const backdrop = ensureAuthShellBackdrop()
expect(backdrop.id).toBe(AUTH_SHELL_BACKDROP_ID)
expect(backdrop.getAttribute('data-testid')).toBe('supertokens-auth-backdrop')
expect(backdrop.style.position).toBe('fixed')
expect(backdrop.style.pointerEvents).toBe('none')
expect(backdrop.style.zIndex).toBe('0')
})
it('removes the auth backdrop when navigation leaves auth routes', () => {
syncAuthShellBackdrop('/login')
expect(document.getElementById(AUTH_SHELL_BACKDROP_ID)).not.toBeNull()
syncAuthShellBackdrop('/pricing')
expect(document.getElementById(AUTH_SHELL_BACKDROP_ID)).toBeNull()
})
it('does not duplicate the backdrop across repeated auth-route syncs', () => {
syncAuthShellBackdrop('/login')
syncAuthShellBackdrop('/register')
syncAuthShellBackdrop('/auth/callback')
expect(document.querySelectorAll(`#${AUTH_SHELL_BACKDROP_ID}`)).toHaveLength(1)
})
})

View file

@ -0,0 +1,46 @@
export const AUTH_SHELL_BACKDROP_ID = 'hypertwist-auth-shell-backdrop'
function applyAuthShellBackdropStyles(backdrop: HTMLElement) {
backdrop.id = AUTH_SHELL_BACKDROP_ID
backdrop.setAttribute('data-testid', 'supertokens-auth-backdrop')
backdrop.setAttribute('aria-hidden', 'true')
backdrop.style.position = 'fixed'
backdrop.style.inset = '0'
backdrop.style.zIndex = '0'
backdrop.style.pointerEvents = 'none'
backdrop.style.background =
'radial-gradient(circle at top, rgba(216,203,175,0.12), transparent 40%), linear-gradient(180deg, rgba(18,20,24,0.97), rgba(10,12,17,0.99))'
}
export function isAuthShellRoute(pathname: string) {
return pathname === '/login'
|| pathname.startsWith('/login/')
|| pathname === '/register'
|| pathname.startsWith('/register/')
|| pathname.startsWith('/auth/')
}
export function ensureAuthShellBackdrop(doc: Document = document) {
const existingBackdrop = doc.getElementById(AUTH_SHELL_BACKDROP_ID)
if (existingBackdrop instanceof HTMLElement) {
applyAuthShellBackdropStyles(existingBackdrop)
return existingBackdrop
}
const backdrop = doc.createElement('div')
applyAuthShellBackdropStyles(backdrop)
doc.body.appendChild(backdrop)
return backdrop
}
export function syncAuthShellBackdrop(pathname: string, doc: Document = document) {
const shouldRender = isAuthShellRoute(pathname)
const existingBackdrop = doc.getElementById(AUTH_SHELL_BACKDROP_ID)
if (!shouldRender) {
existingBackdrop?.remove()
return
}
ensureAuthShellBackdrop(doc)
}

View file

@ -0,0 +1,13 @@
import { useLayoutEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { syncAuthShellBackdrop } from '../auth/auth-shell-backdrop'
export function AuthShellBackdropSync() {
const location = useLocation()
useLayoutEffect(() => {
syncAuthShellBackdrop(location.pathname)
}, [location.pathname])
return null
}