Harden website auth bootstrap and provider parity

This commit is contained in:
axiomlogicnexus 2026-06-27 22:49:14 +00:00
parent b1764be306
commit ce389a97a7
29 changed files with 979 additions and 136 deletions

View file

@ -89,10 +89,13 @@ Current behavior:
- shared SuperTokens auth frontend posture aligned with FamiliarOS and ScriptoriumAI
- email/password login and registration
- optional GitHub and Google OAuth flags
- optional ORCID redirect posture retained client-side as a backend-gated extension point
- optional ORCID now also exists as a bounded first-party auth-server custom-provider lane instead of remaining a client-only placeholder
- deterministic local fallback mode when the backend is not configured
- bounded auth-health probing that now distinguishes configured, reachable, and ready shared-core posture without mutating auth state
- bounded frontend auth-env diagnostics that now warn when the browser lane still points at loopback, insecure `http`, or split backend/auth targets
- auth-health runtime truth now also reports ORCID readiness beside GitHub and Google when that provider is configured
- login and register now both expose the same provider-row continuation and redirect-recovery behavior, so provider sign-in posture is not narrower on registration than on login
- the same-origin bundle renderer now emits `VITE_ORCID_OAUTH_ENABLED` from the authoritative server/provider manifest instead of hardcoding it off in the bundle lane
### Browser dashboard posture
@ -146,6 +149,10 @@ The login and register pages now also surface those shared-auth runtime warnings
instead of silently behaving like production auth when the lane is still in
local fallback or mixed deployment posture.
The public docs route now also carries an explicit browser-account-method guide
so provider truth no longer depends on operators reaching only the auth form
pages to see which shared-auth lanes are actually present in the current build.
The website package now also includes a first-party external runtime-readiness
command under:

View file

@ -259,6 +259,35 @@ Additional follow-up after the protected website hardening continuation:
- `scripts/run-hypertwist-gitnexus-status.sh` then reported the bounded
mirror `Status: up-to-date`
Latest tool refresh on `2026-06-27`:
- the same bounded refactor loop stayed healthy through the shared-auth
provider parity and public-manual continuation packet:
- `scripts/run-hypertwist-sentrux-source-only.sh` passed again with:
- `Quality: 6218`
- all `7` rules passing
- `scripts/run-hypertwist-gitnexus-analyze.sh` again fell back cleanly from
the retained local CLI to `npx -y gitnexus@latest` on this Linux host,
then re-indexed the bounded mirror successfully at:
- `16,310` nodes
- `38,379` edges
- `672` clusters
- `300` flows
- completion time `91.4s`
- `scripts/run-hypertwist-gitnexus-status.sh` then reported:
- `Indexed commit: 15e3608`
- `Current commit: 15e3608`
- `Status: up-to-date`
Current interpretation after that refresh:
- the public/auth/manual widening did not introduce new structural debt
- the repo-local `sentrux` lane remains stable as the day-to-day structural
gate
- the retained GitNexus local runtime is still not usable on this Linux host
because of the native `LadybugDB` payload mismatch, but the HyperTwist-owned
wrapper continues to fail over truthfully instead of masking that host fact
Latest protected packaged-proof continuation refresh on `2026-06-24`:
- `scripts/run-hypertwist-gitnexus-analyze.sh` again fell back cleanly from the
@ -940,6 +969,44 @@ Latest website/operator follow-up later on `2026-06-24`:
cleanup and deeper product/runtime quality review, not as unresolved
toolchain setup debt
Latest refresh on `2026-06-27`:
- the next bounded website/auth refactor then split config-only shared-auth
runtime facts away from heavy SuperTokens recipe imports, moved the route
wrapper onto a lazy auth-only path, and kept sign-in or sign-up or provider
actions on demand-loaded recipe modules instead of front-loading them into
the ordinary public website surface
- the production website build then proved that the remaining auth-core chunk
dropped below the warning threshold:
- `npm --prefix website run build`
- `auth-core-vendor`: `391.18 kB`
- the previous oversized Vite warning no longer appeared
- the owned source-only structural gate also stayed clean and improved again:
- `scripts/run-hypertwist-sentrux-source-only.sh`
- `Quality: 6220`
- all `7` rules pass
- the HyperTwist-owned graph refresh again succeeded on the bounded mirror:
- `scripts/run-hypertwist-gitnexus-analyze.sh`
- retained local CLI again failed cleanly on this Linux host because of the
cross-platform native payload mismatch, so the wrapper truthfully fell back
to `npx -y gitnexus@latest`
- fallback analyze run completed successfully in `93.2s`
- bounded mirror result:
- `16,303` nodes
- `38,349` edges
- `674` clusters
- `300` flows
- `scripts/run-hypertwist-gitnexus-status.sh` then reported:
- `Indexed commit: 5272e18`
- `Current commit: 5272e18`
- `Status: up-to-date`
- current truthful reading after this refresh:
- HyperTwist’s “vanilla refactor” lane is no longer about tool adoption or
a failing browser/public structural gate
- the current higher-value follow-ups are product-quality review, bounded
code ownership cleanup, and intentionally chosen runtime packets rather
than emergency website/auth bundle repair
## Out of scope
This note does not:

View file

@ -34,6 +34,8 @@ Recommended server env posture:
- `WEBSITE_BASE_PATH=/auth`
- `COOKIE_SECURE=true`
- `SERVE_STATIC_WEBSITE=true` when the auth server owns same-origin public delivery
- optional `ORCID_CLIENT_ID` and `ORCID_CLIENT_SECRET` when ORCID browser sign-in is intentionally enabled
- optional `ORCID_IS_SANDBOX=true` for sandbox-only ORCID rehearsal instead of the production ORCID authority
- real `PADDLE_WEBHOOK_SECRET`
- real `PADDLE_PRODUCT_PLAN_MAP` and/or `PADDLE_PRICE_PLAN_MAP`
@ -58,6 +60,16 @@ the readiness command still fails until real launch values are inserted.
They now also carry the explicit static-serving control variables for the
same-origin lane.
Current same-family follow-up on `2026-06-27`:
- HyperTwist now treats ORCID as a real bounded shared-auth provider lane
rather than a client-only extension point
- the same-origin bundle renderer now emits matching frontend plus server ORCID
env values from the authoritative manifest whenever both ORCID credentials
are present
- runtime diagnostics now also warn when ORCID is only partially configured, so
a half-wired provider cannot silently masquerade as launch-ready auth posture
That expectation is now proven two ways in the repo itself:
- the real `check-runtime-readiness` CLI is exercised against the checked-in production example files and must fail until placeholders are replaced
@ -149,7 +161,7 @@ Before public launch, the current runtime and dashboard surfaces should show:
- runtime mode: `public`
- `public auth origin ready: yes`
- no runtime config errors
- no runtime config warnings that indicate loopback, insecure `http`, or partial OAuth setup
- no runtime config warnings that indicate loopback, insecure `http`, or partial GitHub/Google/ORCID OAuth setup
- dashboard launch-readiness issues cleared for download/check-out/source configuration
- the deployed website root includes the first-party shell marker rather than
the old rollout placeholder page

View file

@ -926,6 +926,91 @@ Current audit note:
- `scripts/run-hypertwist-gitnexus-status.sh`
- bounded mirror `Status: up-to-date`
## Latest website bootstrap hardening follow-up (`2026-06-27`)
- the public website entrypoint now initializes shared SuperTokens posture from
`website/src/main.tsx` before React root creation instead of waiting for a
later auth-shell or platform-auth path to trigger first use
- `website/index.html` now also carries a centered first-paint HyperTwist boot
shell inside `#root`, so public and protected routes no longer begin from a
blank document while the first bundle loads
- focused bootstrap validation stayed green under:
- `npm --prefix website test -- --run src/__tests__/main.bootstrap.test.tsx src/__tests__/App.bootstrap.test.tsx`
- `2` test files passed
- `5` tests passed
## Latest website auth bundle hardening follow-up (`2026-06-27`)
- the remaining public-website auth payload warning was then closed without
widening product scope:
- config-only auth runtime facts now live in
`website/src/auth/supertokens-runtime.ts`
- heavy SuperTokens client initialization remains in
`website/src/auth/supertokens-client.ts`, but now loads its large recipe
modules lazily instead of importing them into ordinary public routes
- the auth-route wrapper moved behind
`website/src/auth/SuperTokensRouteWrapper.tsx` and is now lazy-loaded only
when a route actually needs the shared browser-auth shell
- platform-auth login, register, provider-login, and logout flows now use
lazy recipe imports instead of front-loading those modules into the main
website application path
- current production-build truth after that split:
- `npm --prefix website run build`
- no Vite chunk-size warning remained
- `auth-core-vendor` dropped to `391.18 kB`
- the route wrapper itself stayed tiny at `0.21 kB`
- focused auth/bootstrap validation widened and stayed green under:
- `npm --prefix website test -- --run src/__tests__/main.bootstrap.test.tsx src/__tests__/App.bootstrap.test.tsx src/__tests__/platform-auth.bootstrap.test.tsx src/__tests__/public-auth-pages.test.tsx`
- `4` test files passed
- `17` tests passed
- the full same-family web/product gate then stayed green again under:
- `scripts/run-hypertwist-web-surface-validation.sh`
- focused website route/auth/release validation: `12` files, `66` tests
passed
- website/server validation: `10` files, `36` tests passed
- `Content/Browser` verify/build passed
- website and `Content/Browser` production audits stayed clean
- the only retained audit residual remained the already-documented upstream
`supertokens-node -> nodemailer` advisory in `website/server`
## Latest shared-auth provider parity follow-up (`2026-06-27`)
- the remaining HyperTwist shared-auth provider drift then closed in the same
family without widening product scope:
- the auth server now owns a bounded first-party ORCID custom-provider lane
in `website/server/src/index.ts` instead of leaving ORCID as a UI-only
placeholder
- runtime config diagnostics now also cover partial ORCID configuration in
`website/server/src/runtime-config.ts`
- the same-origin bundle renderer now emits matching ORCID frontend/server
env facts from the authoritative manifest instead of hardcoding
`VITE_ORCID_OAUTH_ENABLED=false`
- protected auth-health truth now reports ORCID readiness beside GitHub and
Google
- login and register now both share the same provider-row continuation and
redirect-recovery behavior instead of leaving provider sign-in posture
narrower on registration
- the public docs route now also exposes the real shared browser-auth method
lineup so provider truth is no longer stranded only on the auth entry
pages
- cross-repo source truth for this packet stayed disciplined:
- FamiliarOS current website auth posture remains intentionally narrower
(`email/password` plus `GitHub`)
- ScriptoriumAI current auth server still carries the bounded ORCID custom
provider pattern on the same `supertokens-node@24.0.2` family
- HyperTwist now reuses that bounded provider approach in a first-party way
while keeping desktop-first simulator truth unchanged
- focused validation for the widened packet stayed green under:
- `npm --prefix website test -- --run src/__tests__/public-auth-pages.test.tsx src/__tests__/public-marketing-pages.test.tsx src/__tests__/platform-auth.bootstrap.test.tsx`
- `npm --prefix website/server test -- --run src/__tests__/runtime-config.test.ts src/__tests__/auth-health.test.ts`
- `npm --prefix website run test -- --run scripts/render-same-origin-bundle-lib.test.mjs`
- `npm --prefix website/server run type-check`
- the same-family umbrella and structural gates also remained the target truth:
- `scripts/run-hypertwist-web-surface-validation.sh`
- `scripts/run-hypertwist-sentrux-source-only.sh`
- `scripts/run-hypertwist-gitnexus-analyze.sh`
- `scripts/run-hypertwist-gitnexus-status.sh`
## Latest native/public control-roster parity follow-up (`2026-06-25`)
- the same-family native/operator continuity lane then aligned the shipped

File diff suppressed because one or more lines are too long

View file

@ -32,10 +32,113 @@
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<style>
:root {
color-scheme: dark;
}
body {
margin: 0;
background:
radial-gradient(circle at top, rgba(84, 203, 255, 0.16), transparent 40%),
linear-gradient(180deg, #0b1020, #050814 68%);
color: #f4f6f8;
font-family: "IBM Plex Sans", system-ui, sans-serif;
}
.hypertwist-initial-shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
box-sizing: border-box;
}
.hypertwist-initial-shell__frame {
width: min(30rem, 100%);
border: 1px solid rgba(84, 203, 255, 0.22);
border-radius: 1.75rem;
background: rgba(9, 14, 28, 0.9);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.4);
padding: 2rem 1.5rem;
display: grid;
justify-items: center;
gap: 0.9rem;
text-align: center;
}
.hypertwist-initial-shell__brand {
width: 4.5rem;
height: 4.5rem;
object-fit: contain;
filter: drop-shadow(0 0 22px rgba(84, 203, 255, 0.25));
}
.hypertwist-initial-shell__eyebrow {
margin: 0;
color: #54cbff;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.hypertwist-initial-shell__spinner {
width: 3rem;
height: 3rem;
border-radius: 999px;
border: 3px solid rgba(84, 203, 255, 0.18);
border-top-color: #54cbff;
border-right-color: #f7b267;
animation: hypertwist-initial-shell-spin 0.9s linear infinite;
}
.hypertwist-initial-shell__title {
margin: 0;
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
font-size: clamp(1.55rem, 3vw, 2rem);
font-weight: 700;
letter-spacing: -0.03em;
}
.hypertwist-initial-shell__body {
margin: 0;
max-width: 24rem;
color: rgba(212, 225, 245, 0.86);
font-size: 0.97rem;
line-height: 1.6;
}
@keyframes hypertwist-initial-shell-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<title>HyperTwist</title>
</head>
<body>
<div id="root"></div>
<div id="root">
<div class="hypertwist-initial-shell" role="status" aria-live="polite" aria-label="Loading HyperTwist">
<div class="hypertwist-initial-shell__frame">
<img
src="/branding/hypertwist-3d-symbol.png"
alt="HyperTwist"
class="hypertwist-initial-shell__brand"
/>
<p class="hypertwist-initial-shell__eyebrow">Booting HyperTwist</p>
<div class="hypertwist-initial-shell__spinner" aria-hidden="true"></div>
<p class="hypertwist-initial-shell__title">Loading HyperTwist...</p>
<p class="hypertwist-initial-shell__body">
Preparing the public product story, shared auth posture, and desktop-first release lane.
</p>
</div>
</div>
</div>
<script>
(function () {
const BACKDROP_ID = 'hypertwist-auth-shell-backdrop';

View file

@ -143,6 +143,9 @@ export function resolveBundleManifest(manifest = {}) {
githubClientSecret: normalizeTrimmed(server.githubClientSecret || ''),
googleClientId: normalizeTrimmed(server.googleClientId || ''),
googleClientSecret: normalizeTrimmed(server.googleClientSecret || ''),
orcidClientId: normalizeTrimmed(server.orcidClientId || ''),
orcidClientSecret: normalizeTrimmed(server.orcidClientSecret || ''),
orcidIsSandbox: normalizeBoolean(server.orcidIsSandbox, false),
defaultPlan: normalizeTrimmed(server.defaultPlan || 'free'),
defaultRole: normalizeTrimmed(server.defaultRole || 'operator'),
healthTimeoutMs: normalizeTrimmed(server.healthTimeoutMs || '2000'),
@ -170,7 +173,7 @@ export function buildFrontendEnvEntries(manifest = {}) {
VITE_AUTH_API_TIMEOUT_MS: '8000',
VITE_GITHUB_OAUTH_ENABLED: String(Boolean(resolved.server.githubClientId && resolved.server.githubClientSecret)),
VITE_GOOGLE_OAUTH_ENABLED: String(Boolean(resolved.server.googleClientId && resolved.server.googleClientSecret)),
VITE_ORCID_OAUTH_ENABLED: 'false',
VITE_ORCID_OAUTH_ENABLED: String(Boolean(resolved.server.orcidClientId && resolved.server.orcidClientSecret)),
VITE_PUBLIC_DOCS_URL: resolved.publicDocsUrl,
VITE_RELEASE_NOTES_URL: resolved.releaseNotesUrl,
VITE_SUPPORT_EMAIL: resolved.supportEmail,
@ -204,6 +207,9 @@ export function buildServerEnvEntries(manifest = {}) {
GITHUB_CLIENT_SECRET: resolved.server.githubClientSecret,
GOOGLE_CLIENT_ID: resolved.server.googleClientId,
GOOGLE_CLIENT_SECRET: resolved.server.googleClientSecret,
ORCID_CLIENT_ID: resolved.server.orcidClientId,
ORCID_CLIENT_SECRET: resolved.server.orcidClientSecret,
ORCID_IS_SANDBOX: String(resolved.server.orcidIsSandbox),
DEFAULT_PLAN: resolved.server.defaultPlan,
DEFAULT_ROLE: resolved.server.defaultRole,
SUPERTOKENS_HEALTH_TIMEOUT_MS: resolved.server.healthTimeoutMs,

View file

@ -93,6 +93,19 @@ describe('buildFrontendEnvEntries', () => {
expect(env.VITE_SUPERTOKENS_WEBSITE_DOMAIN).toBe('https://preview.hypertwist.app')
expect(env.VITE_AUTH_API_BASE_URL).toBe('https://preview.hypertwist.app')
})
it('enables ORCID in frontend env only when the server manifest carries a complete ORCID credential pair', () => {
const env = buildFrontendEnvEntries({
...createValidManifest(),
server: {
...createValidManifest().server,
orcidClientId: 'orcid-client-id',
orcidClientSecret: 'orcid-client-secret',
},
})
expect(env.VITE_ORCID_OAUTH_ENABLED).toBe('true')
})
})
describe('buildServerEnvEntries', () => {
@ -115,6 +128,22 @@ describe('buildServerEnvEntries', () => {
expect(env.API_DOMAIN).toBe('https://preview.hypertwist.app')
expect(env.WEBSITE_DOMAIN).toBe('https://preview.hypertwist.app')
})
it('carries ORCID server env through when the manifest declares the custom provider credentials', () => {
const env = buildServerEnvEntries({
...createValidManifest(),
server: {
...createValidManifest().server,
orcidClientId: 'orcid-client-id',
orcidClientSecret: 'orcid-client-secret',
orcidIsSandbox: true,
},
})
expect(env.ORCID_CLIENT_ID).toBe('orcid-client-id')
expect(env.ORCID_CLIENT_SECRET).toBe('orcid-client-secret')
expect(env.ORCID_IS_SANDBOX).toBe('true')
})
})
describe('buildBundleValidationReport', () => {

View file

@ -41,11 +41,13 @@ describe('getRuntimeConfigDiagnostics', () => {
superTokensCoreUri: 'http://localhost:3567',
cookieSecure: false,
googleClientId: 'only-client-id',
orcidClientId: 'only-orcid-client-id',
})
expect(diagnostics.publicOriginReady).toBe(false)
expect(diagnostics.errors).toContain('API_DOMAIN must not include a path; use API_BASE_PATH for route prefixing.')
expect(diagnostics.errors).toContain('WEBSITE_DOMAIN must be an absolute URL origin.')
expect(diagnostics.warnings).toContain('Google OAuth is only partially configured; set both client ID and client secret or neither.')
expect(diagnostics.warnings).toContain('ORCID OAuth is only partially configured; set both client ID and client secret or neither.')
})
})

View file

@ -23,6 +23,9 @@ import { createSessionLikeFromTestPayload, readSignedTestSessionFromRequest, typ
const Github = GithubProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GithubProvider>
const Google = GoogleProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GoogleProvider>
type ThirdPartyProviderList = NonNullable<
NonNullable<NonNullable<Parameters<typeof ThirdParty.init>[0]>['signInAndUpFeature']>['providers']
>
const PORT = Number(process.env.PORT || 3001)
const SUPERTOKENS_CORE_URI = process.env.SUPERTOKENS_CORE_URI || 'http://localhost:3567'
@ -35,6 +38,9 @@ const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
const ORCID_CLIENT_ID = process.env.ORCID_CLIENT_ID || ''
const ORCID_CLIENT_SECRET = process.env.ORCID_CLIENT_SECRET || ''
const ORCID_IS_SANDBOX = String(process.env.ORCID_IS_SANDBOX || '').toLowerCase() === 'true'
const DEFAULT_PLAN = normalizeBillingPlan(process.env.DEFAULT_PLAN || 'free')
const DEFAULT_ROLE = normalizeBillingRole(process.env.DEFAULT_ROLE || 'operator')
const SUPERTOKENS_HEALTH_TIMEOUT_MS = Number(process.env.SUPERTOKENS_HEALTH_TIMEOUT_MS || 2_000)
@ -63,6 +69,8 @@ const runtimeConfigDiagnostics = getRuntimeConfigDiagnostics({
githubClientSecret: GITHUB_CLIENT_SECRET,
googleClientId: GOOGLE_CLIENT_ID,
googleClientSecret: GOOGLE_CLIENT_SECRET,
orcidClientId: ORCID_CLIENT_ID,
orcidClientSecret: ORCID_CLIENT_SECRET,
})
function normalizeBillingPlan(value: string): BillingPlan {
@ -150,6 +158,64 @@ async function getRequestSession(req: SessionRequest, res: express.Response): Pr
return (await Session.getSession(req, res, { sessionRequired: false })) ?? null
}
function createOrcidProvider() {
const orcidBase = ORCID_IS_SANDBOX ? 'https://sandbox.orcid.org' : 'https://orcid.org'
return {
id: 'orcid',
name: 'ORCID',
get(redirectURI: string | undefined, authCodeFromRequest: string | undefined) {
return {
accessTokenAPI: {
url: `${orcidBase}/oauth/token`,
params: {
client_id: ORCID_CLIENT_ID,
client_secret: ORCID_CLIENT_SECRET,
grant_type: 'authorization_code',
redirect_uri: redirectURI || '',
code: authCodeFromRequest || '',
},
},
authorisationRedirect: {
url: `${orcidBase}/oauth/authorize`,
params: {
client_id: ORCID_CLIENT_ID,
response_type: 'code',
scope: '/authenticate',
redirect_uri: redirectURI || '',
},
},
getUserInfo: async (tokenAPIResponse: {
access_token?: string
orcid?: string
name?: string
email?: string
}) => {
const orcidId = String(tokenAPIResponse.orcid || '').trim()
const fallbackEmailLocalPart = orcidId.replace(/\W/g, '') || 'orcid-user'
return {
thirdPartyUserId: orcidId,
email: {
id: tokenAPIResponse.email
? String(tokenAPIResponse.email).trim().toLowerCase()
: `${fallbackEmailLocalPart}@orcid.placeholder`,
isVerified: true,
},
rawUserInfoFromProvider: {
fromUserInfoAPI: {
orcidId,
name: String(tokenAPIResponse.name || '').trim(),
},
fromIdTokenPayload: {},
},
}
},
}
},
}
}
const recipeList = [
EmailPassword.init({
signUpFeature: {
@ -181,7 +247,7 @@ const recipeList = [
}),
]
const thirdPartyProviders = []
const thirdPartyProviders: Array<ReturnType<typeof Github> | ReturnType<typeof Google> | ReturnType<typeof createOrcidProvider>> = []
if (GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET) {
thirdPartyProviders.push(Github({
clientId: GITHUB_CLIENT_ID,
@ -196,11 +262,14 @@ if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
scope: ['openid', 'email', 'profile'],
}))
}
if (ORCID_CLIENT_ID && ORCID_CLIENT_SECRET) {
thirdPartyProviders.push(createOrcidProvider())
}
if (thirdPartyProviders.length > 0) {
recipeList.splice(1, 0, ThirdParty.init({
signInAndUpFeature: {
providers: thirdPartyProviders,
providers: thirdPartyProviders as ThirdPartyProviderList,
},
override: {
apis: (originalImplementation) => ({
@ -353,6 +422,7 @@ app.get('/api/auth/health', async (_req, res) => {
oauth: {
github: Boolean(GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET),
google: Boolean(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET),
orcid: Boolean(ORCID_CLIENT_ID && ORCID_CLIENT_SECRET),
},
},
fallback: {

View file

@ -7,6 +7,8 @@ interface RuntimeConfigInput {
githubClientSecret?: string
googleClientId?: string
googleClientSecret?: string
orcidClientId?: string
orcidClientSecret?: string
}
export interface RuntimeConfigDiagnostics {
@ -65,6 +67,8 @@ export function getRuntimeConfigDiagnostics({
githubClientSecret,
googleClientId,
googleClientSecret,
orcidClientId,
orcidClientSecret,
}: RuntimeConfigInput): RuntimeConfigDiagnostics {
const warnings: string[] = []
const errors: string[] = []
@ -131,6 +135,10 @@ export function getRuntimeConfigDiagnostics({
warnings.push('Google OAuth is only partially configured; set both client ID and client secret or neither.')
}
if (!hasCompleteCredentialPair(orcidClientId, orcidClientSecret) && (orcidClientId || orcidClientSecret)) {
warnings.push('ORCID OAuth is only partially configured; set both client ID and client secret or neither.')
}
const mode: RuntimeConfigDiagnostics['mode'] = allLoopback
? 'local'
: noLoopback

View file

@ -1,14 +1,18 @@
import { Suspense, lazy } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter, useLocation } from 'react-router-dom'
import { SuperTokensWrapper } from 'supertokens-auth-react'
import { ErrorBoundary } from './components/ErrorBoundary'
import { PlatformAuthProvider } from './auth/platform-auth'
import { isSuperTokensConfigured } from './auth/supertokens-client'
import { isSuperTokensConfigured } from './auth/supertokens-runtime'
import { AppRouteTree } from './router/AppRouteTree'
import { AuthShellBackdropSync } from './router/AuthShellBackdropSync'
import { ROUTER_FUTURE_FLAGS } from './router/router-future'
import './styles/global.css'
const LazySuperTokensRouteWrapper = lazy(async () => ({
default: (await import('./auth/SuperTokensRouteWrapper')).SuperTokensRouteWrapper,
}))
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -49,7 +53,11 @@ function MaybeSuperTokensWrapper({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
return <SuperTokensWrapper>{children}</SuperTokensWrapper>
return (
<Suspense fallback={<>{children}</>}>
<LazySuperTokensRouteWrapper>{children}</LazySuperTokensRouteWrapper>
</Suspense>
)
}
export default function App() {

View file

@ -9,8 +9,8 @@ vi.mock('../auth/platform-auth', () => ({
usePlatformAuth: () => mockUsePlatformAuth(),
}))
vi.mock('../auth/supertokens-client', async (importOriginal) => {
const actual = await importOriginal<typeof import('../auth/supertokens-client')>()
vi.mock('../auth/supertokens-runtime', async (importOriginal) => {
const actual = await importOriginal<typeof import('../auth/supertokens-runtime')>()
return {
...actual,
isSuperTokensConfigured: () => mockIsSuperTokensConfigured(),
@ -25,8 +25,8 @@ vi.mock('../auth/supertokens-client', async (importOriginal) => {
}
})
vi.mock('supertokens-auth-react', () => ({
SuperTokensWrapper: ({ children }: { children: React.ReactNode }) => (
vi.mock('../auth/SuperTokensRouteWrapper', () => ({
SuperTokensRouteWrapper: ({ children }: { children: React.ReactNode }) => (
<div data-testid="supertokens-wrapper">{children}</div>
),
}))

View file

@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockEnsureSuperTokensInit = vi.fn()
const mockRender = vi.fn()
const mockCreateRoot = vi.fn((_: Element | DocumentFragment | null) => ({
render: mockRender,
}))
vi.mock('../auth/supertokens-client', () => ({
ensureSuperTokensInit: () => mockEnsureSuperTokensInit(),
}))
vi.mock('../App', () => ({
default: () => null,
}))
vi.mock('react-dom/client', () => ({
default: {
createRoot: (element: Element | DocumentFragment | null) => mockCreateRoot(element),
},
}))
describe('main bootstrap', () => {
beforeEach(() => {
vi.resetModules()
mockEnsureSuperTokensInit.mockReset()
mockCreateRoot.mockClear()
mockRender.mockClear()
document.body.innerHTML = '<div id="root"><div id="hypertwist-initial-shell">Boot</div></div>'
})
it('initializes shared auth before creating the React root', async () => {
await import('../main')
await Promise.resolve()
const root = document.getElementById('root')
expect(mockEnsureSuperTokensInit).toHaveBeenCalledTimes(1)
expect(mockCreateRoot).toHaveBeenCalledWith(root)
expect(mockRender).toHaveBeenCalledTimes(1)
expect(mockEnsureSuperTokensInit.mock.invocationCallOrder[0]).toBeLessThan(
mockCreateRoot.mock.invocationCallOrder[0],
)
})
})

View file

@ -14,22 +14,16 @@ vi.mock('../auth/auth-api', () => ({
vi.mock('../auth/supertokens-client', () => ({
ensureSuperTokensInit: (...args: unknown[]) => mockEnsureSuperTokensInit(...args),
signInWithSuperTokens: vi.fn(),
signUpWithSuperTokens: vi.fn(),
redirectToSuperTokensThirdPartyLogin: vi.fn(),
signOutFromSuperTokens: vi.fn(),
}))
vi.mock('../auth/supertokens-runtime', () => ({
isSuperTokensConfigured: () => mockIsSuperTokensConfigured(),
}))
vi.mock('supertokens-auth-react/recipe/emailpassword', () => ({
signIn: vi.fn(),
signUp: vi.fn(),
}))
vi.mock('supertokens-auth-react/recipe/thirdparty', () => ({
redirectToThirdPartyLogin: vi.fn(),
}))
vi.mock('supertokens-auth-react/recipe/session', () => ({
signOut: vi.fn(),
}))
import { PlatformAuthProvider, usePlatformAuth } from '../auth/platform-auth'
const AUTH_STORAGE_KEY = 'hypertwist.platform.user.v1'

View file

@ -24,7 +24,7 @@ vi.mock('../auth/auth-api', () => ({
getReleaseManifest: (...args: unknown[]) => mockGetReleaseManifest(...args),
}))
vi.mock('../auth/supertokens-client', () => ({
vi.mock('../auth/supertokens-runtime', () => ({
getSuperTokensAuthRuntimeValidation: () => mockGetSuperTokensAuthRuntimeValidation(),
isGitHubOAuthEnabled: () => mockIsGitHubOAuthEnabled(),
isGoogleOAuthEnabled: () => mockIsGoogleOAuthEnabled(),
@ -317,6 +317,23 @@ describe('public auth and download pages', () => {
expect(mockLogin).toHaveBeenCalledWith({ method: 'orcid', email: '' })
})
it('shows the same enabled OAuth providers on the register page and routes them through the shared login handler', async () => {
mockIsGoogleOAuthEnabled.mockReturnValue(true)
mockIsGitHubOAuthEnabled.mockReturnValue(true)
mockIsOrcidOAuthEnabled.mockReturnValue(true)
mockLogin.mockResolvedValue({ ok: true })
renderAuthRoutes('/register')
await userEvent.click(screen.getByRole('button', { name: 'Continue with Google' }))
await userEvent.click(screen.getByRole('button', { name: 'Continue with GitHub' }))
await userEvent.click(screen.getByRole('button', { name: 'Continue with ORCID' }))
expect(mockLogin).toHaveBeenCalledWith({ method: 'google', email: '' })
expect(mockLogin).toHaveBeenCalledWith({ method: 'github', email: '' })
expect(mockLogin).toHaveBeenCalledWith({ method: 'orcid', email: '' })
})
it('keeps configured public download targets behind the protected dashboard instead of exposing raw URLs', async () => {
renderDownloadPage()

View file

@ -6,6 +6,10 @@ import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
const mockGetReleaseManifest = vi.fn()
const mockGetAuthHealth = vi.fn()
const mockGetSuperTokensAuthRuntimeValidation = vi.fn()
const mockIsGitHubOAuthEnabled = vi.fn(() => false)
const mockIsGoogleOAuthEnabled = vi.fn(() => false)
const mockIsOrcidOAuthEnabled = vi.fn(() => false)
vi.mock('../auth/platform-auth', () => ({
usePlatformAuth: () => ({
@ -18,6 +22,13 @@ vi.mock('../auth/auth-api', () => ({
getReleaseManifest: (...args: unknown[]) => mockGetReleaseManifest(...args),
}))
vi.mock('../auth/supertokens-runtime', () => ({
getSuperTokensAuthRuntimeValidation: () => mockGetSuperTokensAuthRuntimeValidation(),
isGitHubOAuthEnabled: () => mockIsGitHubOAuthEnabled(),
isGoogleOAuthEnabled: () => mockIsGoogleOAuthEnabled(),
isOrcidOAuthEnabled: () => mockIsOrcidOAuthEnabled(),
}))
vi.mock('../site-config', () => ({
brandConfig: {
brandName: 'HyperTwist',
@ -140,6 +151,18 @@ describe('public marketing pages', () => {
cleanup()
mockGetAuthHealth.mockReset()
mockGetReleaseManifest.mockReset()
mockGetSuperTokensAuthRuntimeValidation.mockReset()
mockIsGitHubOAuthEnabled.mockReset()
mockIsGoogleOAuthEnabled.mockReset()
mockIsOrcidOAuthEnabled.mockReset()
mockGetSuperTokensAuthRuntimeValidation.mockReturnValue({
ready: true,
missing: [],
warnings: [],
})
mockIsGitHubOAuthEnabled.mockReturnValue(true)
mockIsGoogleOAuthEnabled.mockReturnValue(false)
mockIsOrcidOAuthEnabled.mockReturnValue(true)
})
it('routes public download access through the protected dashboard with the requested platform preserved', async () => {
@ -1108,6 +1131,74 @@ describe('public marketing pages', () => {
expect(screen.getByRole('link', { name: 'https://docs.hypertwist.app' })).toBeTruthy()
})
it('shows the current browser account method lineup on the public docs page', async () => {
mockGetAuthHealth.mockResolvedValue({
ok: true,
service: 'hypertwist-auth-server',
supertokens: {
configured: true,
reachable: true,
ready: true,
apiVersion: '5.4',
error: null,
oauth: {
github: true,
google: false,
orcid: true,
},
},
fallback: {
enabled: true,
active: false,
reason: null,
},
billing: {
statePath: '/var/lib/hypertwist/auth/hypertwist-billing-state.json',
processedEventCount: 0,
pricePlanMapConfigured: false,
productPlanMapConfigured: false,
webhookSecretConfigured: false,
},
runtime: {
mode: 'mixed',
public_origin_ready: true,
cookie_secure: true,
api_domain: 'https://hypertwist.app',
website_domain: 'https://hypertwist.app',
warnings: [],
errors: [],
},
})
mockGetReleaseManifest.mockResolvedValue({
ok: true,
manifest: {
generated_at: '2026-06-22T12:00:00.000Z',
support_email: 'hello@hypertwist.app',
public_docs_url: 'https://docs.hypertwist.app',
release_notes_url: 'https://notes.hypertwist.app',
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
viewer: {
authenticated: false,
canDownload: false,
plan: null,
role: null,
accessStatus: null,
},
platforms: [],
},
})
renderWithProviders(<DocsPage />)
expect(screen.getByRole('heading', { name: 'Browser account access methods' })).toBeTruthy()
expect(screen.getByText('Auth methods ready')).toBeTruthy()
expect(screen.getByText('Email and password')).toBeTruthy()
expect(screen.getByText('GitHub sign-in')).toBeTruthy()
expect(screen.getByText('ORCID sign-in')).toBeTruthy()
expect(screen.queryByText('Google sign-in')).toBeNull()
})
it('renders the public operator manual on the docs page', async () => {
mockGetAuthHealth.mockResolvedValue({
ok: true,

View file

@ -0,0 +1,6 @@
import type { ReactNode } from 'react'
import { SuperTokensWrapper } from 'supertokens-auth-react'
export function SuperTokensRouteWrapper({ children }: { children: ReactNode }) {
return <SuperTokensWrapper>{children}</SuperTokensWrapper>
}

View file

@ -68,6 +68,7 @@ export interface AuthHealthPayload {
oauth?: {
github: boolean
google: boolean
orcid?: boolean
}
}
fallback: {

View file

@ -8,17 +8,21 @@ import {
type ReactNode,
type SetStateAction,
} from 'react'
import { signIn as superTokensSignIn, signUp as superTokensSignUp } from 'supertokens-auth-react/recipe/emailpassword'
import { redirectToThirdPartyLogin } from 'supertokens-auth-react/recipe/thirdparty'
import { signOut as superTokensSignOut } from 'supertokens-auth-react/recipe/session'
import {
getCurrentUser,
logoutCurrentUser,
type ApiBootstrapUserPayload,
type ReleaseManifestPayload,
} from './auth-api'
import { ensureSuperTokensInit, isSuperTokensConfigured } from './supertokens-client'
import {
ensureSuperTokensInit,
redirectToSuperTokensThirdPartyLogin,
signInWithSuperTokens,
signOutFromSuperTokens,
signUpWithSuperTokens,
} from './supertokens-client'
import { validateStrongPassword } from './password-policy'
import { isSuperTokensConfigured } from './supertokens-runtime'
export type AuthMethod = 'supertokens' | 'email' | 'github' | 'google' | 'orcid'
export type ColorMode = 'dark' | 'light'
@ -421,7 +425,10 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
releaseAuthoritySyncItems,
async login(input) {
if (input.method === 'github' || input.method === 'google' || input.method === 'orcid') {
await redirectToThirdPartyLogin({ thirdPartyId: input.method })
if (!superTokensConfigured) {
return { ok: false, error: 'Shared browser auth is not fully configured yet.' }
}
await redirectToSuperTokensThirdPartyLogin(input.method)
return { ok: true }
}
@ -437,8 +444,8 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
return { ok: true }
}
ensureSuperTokensInit()
const response = await superTokensSignIn({
await ensureSuperTokensInit()
const response = await signInWithSuperTokens({
formFields: [
{ id: 'email', value: safeEmail },
{ id: 'password', value: String(input.password || '') },
@ -479,8 +486,8 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
return { ok: true }
}
ensureSuperTokensInit()
const response = await superTokensSignUp({
await ensureSuperTokensInit()
const response = await signUpWithSuperTokens({
formFields: [
{ id: 'email', value: safeEmail },
{ id: 'password', value: input.password },
@ -509,8 +516,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
if (!superTokensConfigured) {
return
}
ensureSuperTokensInit()
await redirectToThirdPartyLogin({ thirdPartyId: provider })
await redirectToSuperTokensThirdPartyLogin(provider)
},
async logout() {
clearSessionState(setUser, setReleaseAuthoritySyncItems)
@ -523,7 +529,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
// Keep local logout deterministic even if the API is unavailable.
}
try {
await superTokensSignOut()
await signOutFromSuperTokens()
} catch {
// Session revocation can already have happened server-side.
}

View file

@ -1,23 +1,17 @@
import SuperTokens from 'supertokens-auth-react'
import EmailPassword from 'supertokens-auth-react/recipe/emailpassword'
import ThirdParty, { Github, Google } from 'supertokens-auth-react/recipe/thirdparty'
import Session from 'supertokens-auth-react/recipe/session'
import { getAuthRuntimeConfig, validateAuthRuntimeConfig } from './auth-env'
import { normalizeNextPath } from './next-path'
import { PASSWORD_POLICY_HINT, PASSWORD_POLICY_PLACEHOLDER, validateStrongPassword } from './password-policy'
const authRuntime = getAuthRuntimeConfig()
const authRuntimeValidation = validateAuthRuntimeConfig()
const SUPERTOKENS_API_DOMAIN = authRuntime.superTokensApiDomain
const SUPERTOKENS_WEBSITE_DOMAIN = authRuntime.superTokensWebsiteDomain
const SUPERTOKENS_API_BASE_PATH = authRuntime.superTokensApiBasePath
const SUPERTOKENS_WEBSITE_BASE_PATH = authRuntime.superTokensWebsiteBasePath
const GITHUB_OAUTH_ENABLED = String((import.meta.env as Record<string, string>).VITE_GITHUB_OAUTH_ENABLED || '').toLowerCase() === 'true'
const GOOGLE_OAUTH_ENABLED = String((import.meta.env as Record<string, string>).VITE_GOOGLE_OAUTH_ENABLED || '').toLowerCase() === 'true'
const ORCID_OAUTH_ENABLED = String((import.meta.env as Record<string, string>).VITE_ORCID_OAUTH_ENABLED || '').toLowerCase() === 'true'
import {
isGitHubOAuthEnabled,
isGoogleOAuthEnabled,
isSuperTokensConfigured,
SUPERTOKENS_API_BASE_PATH,
SUPERTOKENS_API_DOMAIN,
SUPERTOKENS_WEBSITE_BASE_PATH,
SUPERTOKENS_WEBSITE_DOMAIN,
} from './supertokens-runtime'
let initialized = false
let initPromise: Promise<void> | null = null
function readRequestedPostAuthPath() {
if (typeof window === 'undefined') return null
@ -71,85 +65,129 @@ const SUPERTOKENS_BRAND_STYLE = `
}
`
export function isSuperTokensConfigured() {
return Boolean(SUPERTOKENS_API_DOMAIN)
type SuperTokensAuthResponse = {
status?: string
formFields?: Array<{ error?: string }>
}
export function isGitHubOAuthEnabled() {
return GITHUB_OAUTH_ENABLED
type SuperTokensFormField = {
id: string
value: string
}
export function isGoogleOAuthEnabled() {
return GOOGLE_OAUTH_ENABLED
type SuperTokensFormInput = {
formFields: SuperTokensFormField[]
}
export function isOrcidOAuthEnabled() {
return ORCID_OAUTH_ENABLED
}
type SuperTokensThirdPartyId = 'github' | 'google' | 'orcid'
export function getSuperTokensAuthRuntimeValidation() {
return authRuntimeValidation
}
export function ensureSuperTokensInit() {
export async function ensureSuperTokensInit() {
if (initialized || !isSuperTokensConfigured() || typeof window === 'undefined') {
return
}
const thirdPartyProviders = []
if (GITHUB_OAUTH_ENABLED) thirdPartyProviders.push(Github.init())
if (GOOGLE_OAUTH_ENABLED) thirdPartyProviders.push(Google.init())
if (!initPromise) {
initPromise = (async () => {
const [
{ default: SuperTokens },
{ default: EmailPassword },
thirdPartyModule,
{ default: Session },
] = await Promise.all([
import('supertokens-auth-react'),
import('supertokens-auth-react/recipe/emailpassword'),
import('supertokens-auth-react/recipe/thirdparty'),
import('supertokens-auth-react/recipe/session'),
])
const recipeList: Parameters<typeof SuperTokens.init>[0]['recipeList'] = [
EmailPassword.init({
signInAndUpFeature: {
signInForm: {
formFields: [
{ id: 'email', label: 'Email', placeholder: 'operator@hypertwist.app' },
{ id: 'password', label: 'Password', placeholder: 'Your password' },
],
},
signUpForm: {
formFields: [
{ id: 'email', label: 'Email', placeholder: 'operator@hypertwist.app' },
{
id: 'password',
label: `Password (${PASSWORD_POLICY_HINT})`,
placeholder: PASSWORD_POLICY_PLACEHOLDER,
validate: async (value: unknown) => validateStrongPassword(String(value || '')),
const { default: ThirdParty, Github, Google } = thirdPartyModule
const thirdPartyProviders = []
if (isGitHubOAuthEnabled()) thirdPartyProviders.push(Github.init())
if (isGoogleOAuthEnabled()) thirdPartyProviders.push(Google.init())
const recipeList: Parameters<typeof SuperTokens.init>[0]['recipeList'] = [
EmailPassword.init({
signInAndUpFeature: {
signInForm: {
formFields: [
{ id: 'email', label: 'Email', placeholder: 'operator@hypertwist.app' },
{ id: 'password', label: 'Password', placeholder: 'Your password' },
],
},
],
},
},
}),
Session.init(),
]
signUpForm: {
formFields: [
{ id: 'email', label: 'Email', placeholder: 'operator@hypertwist.app' },
{
id: 'password',
label: `Password (${PASSWORD_POLICY_HINT})`,
placeholder: PASSWORD_POLICY_PLACEHOLDER,
validate: async (value: unknown) => validateStrongPassword(String(value || '')),
},
],
},
},
}),
Session.init(),
]
if (thirdPartyProviders.length > 0) {
recipeList.splice(1, 0, ThirdParty.init({
signInAndUpFeature: {
providers: thirdPartyProviders,
},
}))
if (thirdPartyProviders.length > 0) {
recipeList.splice(1, 0, ThirdParty.init({
signInAndUpFeature: {
providers: thirdPartyProviders,
},
}))
}
SuperTokens.init({
appInfo: {
appName: 'HyperTwist',
apiDomain: SUPERTOKENS_API_DOMAIN,
websiteDomain: SUPERTOKENS_WEBSITE_DOMAIN || window.location.origin,
apiBasePath: SUPERTOKENS_API_BASE_PATH,
websiteBasePath: SUPERTOKENS_WEBSITE_BASE_PATH,
},
recipeList,
style: SUPERTOKENS_BRAND_STYLE,
getRedirectionURL: async (context) => {
if (context.action === 'SUCCESS') {
return readRequestedPostAuthPath() || '/app'
}
return undefined
},
})
initialized = true
})().finally(() => {
if (!initialized) {
initPromise = null
}
})
}
SuperTokens.init({
appInfo: {
appName: 'HyperTwist',
apiDomain: SUPERTOKENS_API_DOMAIN,
websiteDomain: SUPERTOKENS_WEBSITE_DOMAIN || window.location.origin,
apiBasePath: SUPERTOKENS_API_BASE_PATH,
websiteBasePath: SUPERTOKENS_WEBSITE_BASE_PATH,
},
recipeList,
style: SUPERTOKENS_BRAND_STYLE,
getRedirectionURL: async (context) => {
if (context.action === 'SUCCESS') {
return readRequestedPostAuthPath() || '/app'
}
return undefined
},
})
initialized = true
await initPromise
}
export async function signInWithSuperTokens(input: SuperTokensFormInput) {
await ensureSuperTokensInit()
const { signIn } = await import('supertokens-auth-react/recipe/emailpassword')
return signIn(input) as Promise<SuperTokensAuthResponse>
}
export async function signUpWithSuperTokens(input: SuperTokensFormInput) {
await ensureSuperTokensInit()
const { signUp } = await import('supertokens-auth-react/recipe/emailpassword')
return signUp(input) as Promise<SuperTokensAuthResponse>
}
export async function redirectToSuperTokensThirdPartyLogin(thirdPartyId: SuperTokensThirdPartyId) {
await ensureSuperTokensInit()
const { redirectToThirdPartyLogin } = await import('supertokens-auth-react/recipe/thirdparty')
await redirectToThirdPartyLogin({ thirdPartyId })
}
export async function signOutFromSuperTokens() {
await ensureSuperTokensInit()
const { signOut } = await import('supertokens-auth-react/recipe/session')
await signOut()
}

View file

@ -0,0 +1,35 @@
import { getAuthRuntimeConfig, validateAuthRuntimeConfig } from './auth-env'
const authRuntime = getAuthRuntimeConfig()
const authRuntimeValidation = validateAuthRuntimeConfig()
const env = import.meta.env as Record<string, string | undefined>
export const SUPERTOKENS_API_DOMAIN = authRuntime.superTokensApiDomain
export const SUPERTOKENS_WEBSITE_DOMAIN = authRuntime.superTokensWebsiteDomain
export const SUPERTOKENS_API_BASE_PATH = authRuntime.superTokensApiBasePath
export const SUPERTOKENS_WEBSITE_BASE_PATH = authRuntime.superTokensWebsiteBasePath
const GITHUB_OAUTH_ENABLED = String(env.VITE_GITHUB_OAUTH_ENABLED || '').toLowerCase() === 'true'
const GOOGLE_OAUTH_ENABLED = String(env.VITE_GOOGLE_OAUTH_ENABLED || '').toLowerCase() === 'true'
const ORCID_OAUTH_ENABLED = String(env.VITE_ORCID_OAUTH_ENABLED || '').toLowerCase() === 'true'
export function isSuperTokensConfigured() {
return Boolean(SUPERTOKENS_API_DOMAIN)
}
export function isGitHubOAuthEnabled() {
return GITHUB_OAUTH_ENABLED
}
export function isGoogleOAuthEnabled() {
return GOOGLE_OAUTH_ENABLED
}
export function isOrcidOAuthEnabled() {
return ORCID_OAUTH_ENABLED
}
export function getSuperTokensAuthRuntimeValidation() {
return authRuntimeValidation
}

View file

@ -0,0 +1,117 @@
import {
getSuperTokensAuthRuntimeValidation,
isGitHubOAuthEnabled,
isGoogleOAuthEnabled,
isOrcidOAuthEnabled,
} from '../../auth/supertokens-runtime'
type BrowserAuthMethodCard = {
title: string
description: string
bullets: readonly string[]
}
function buildBrowserAuthMethodCards(): readonly BrowserAuthMethodCard[] {
const cards: BrowserAuthMethodCard[] = [
{
title: 'Email and password',
description:
'This is the default browser-account lane and remains the most predictable shared-auth path across preview, mixed, and production deployment postures.',
bullets: [
'Use it when you want the clearest route into the protected dashboard, protected downloads, and desktop-link pairing.',
'It opens the same protected account and release surfaces as the optional provider buttons.',
'It does not change the desktop-first simulator boundary or replace native pairing.',
],
},
]
if (isGitHubOAuthEnabled()) {
cards.push({
title: 'GitHub sign-in',
description:
'GitHub is available in this build as an optional browser-account shortcut when the shared auth server has that provider configured.',
bullets: [
'Use it to enter the same protected dashboard and release lanes without creating a separate password first.',
'It still relies on the same shared auth runtime, entitlement checks, and desktop-link handoff as the email/password lane.',
],
})
}
if (isGoogleOAuthEnabled()) {
cards.push({
title: 'Google sign-in',
description:
'Google is available in this build as an optional shared-auth provider for browser account continuity.',
bullets: [
'Use it when you want the same protected download, account, and rollout surfaces through a Google-backed sign-in flow.',
'It does not widen browser ownership into simulator execution or change the desktop pairing boundary.',
],
})
}
if (isOrcidOAuthEnabled()) {
cards.push({
title: 'ORCID sign-in',
description:
'ORCID is available in this build as a first-party shared-auth provider rather than a UI-only placeholder, so research-oriented identity can route through the same protected operator lanes.',
bullets: [
'Use it when this deployment has ORCID configured and you want standards-backed browser sign-in continuity into the protected dashboard and download lanes.',
'It remains a browser-account path only; it does not replace desktop-link pairing or the native simulator runtime.',
],
})
}
return cards
}
export function BrowserAuthMethodsGuide() {
const authRuntimeValidation = getSuperTokensAuthRuntimeValidation()
const cards = buildBrowserAuthMethodCards()
const diagnostics = [
...authRuntimeValidation.missing.map((item) => `Missing auth env: ${item}`),
...authRuntimeValidation.warnings,
]
const statusLabel = authRuntimeValidation.ready
? (diagnostics.length === 0 ? 'Auth methods ready' : 'Auth methods mixed')
: 'Auth methods bounded'
const summary = authRuntimeValidation.ready
? (
diagnostics.length === 0
? 'The shared browser-auth runtime is configured cleanly in this build, so the methods below all route into the same protected account, release, and desktop-pairing surfaces.'
: 'The shared browser-auth runtime is present in this build, but remaining warnings should be cleared before this surface is treated as fully production-ready.'
)
: 'This build may still rely on bounded fallback posture until the missing shared-auth runtime values are configured, even though the method lineup below stays useful as operator guidance.'
return (
<div>
<article className="callout">
<p className={`status-pill${authRuntimeValidation.ready && diagnostics.length === 0 ? ' status-pill--success' : ' status-pill--info'}`}>
{statusLabel}
</p>
<p>{summary}</p>
{diagnostics.length > 0 ? (
<ul className="list top-gap">
{diagnostics.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : null}
</article>
<div className="card-grid top-gap">
{cards.map((card) => (
<article key={card.title} className="card">
<h3>{card.title}</h3>
<p>{card.description}</p>
<ul className="list top-gap">
{card.bullets.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</article>
))}
</div>
</div>
)
}

View file

@ -1,9 +1,16 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ensureSuperTokensInit } from './auth/supertokens-client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
async function bootstrap() {
await ensureSuperTokensInit()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
}
void bootstrap()

View file

@ -953,7 +953,7 @@ export function DashboardOverviewPage() {
<li>Fallback enabled: {healthQuery.data.fallback.enabled ? 'yes' : 'no'}</li>
<li>Fallback active: {healthQuery.data.fallback.active ? 'yes' : 'no'}</li>
<li>Core API version: {healthQuery.data.supertokens.apiVersion || 'unavailable'}</li>
<li>OAuth routes: GitHub {healthQuery.data.supertokens.oauth?.github ? 'on' : 'off'}, Google {healthQuery.data.supertokens.oauth?.google ? 'on' : 'off'}</li>
<li>OAuth routes: GitHub {healthQuery.data.supertokens.oauth?.github ? 'on' : 'off'}, Google {healthQuery.data.supertokens.oauth?.google ? 'on' : 'off'}, ORCID {healthQuery.data.supertokens.oauth?.orcid ? 'on' : 'off'}</li>
<li>Runtime mode: {healthQuery.data.runtime.mode}</li>
<li>Cookie secure: {healthQuery.data.runtime.cookie_secure ? 'yes' : 'no'}</li>
<li>Public auth origin ready: {healthQuery.data.runtime.public_origin_ready ? 'yes' : 'no'}</li>

View file

@ -7,7 +7,7 @@ import {
isGitHubOAuthEnabled,
isGoogleOAuthEnabled,
isOrcidOAuthEnabled,
} from '../auth/supertokens-client'
} from '../auth/supertokens-runtime'
import { SiteMetadata } from '../components/seo/SiteMetadata'
import { OperationalStatusCallout } from '../components/ui/OperationalStatusCallout'
import { browserDesktopRealityCards, deliverySurfaceCards, operatorManualTracks } from '../site-data'
@ -259,6 +259,7 @@ export function LoginPage() {
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [oauthLoading, setOauthLoading] = useState<'google' | 'github' | 'orcid' | null>(null)
useEffect(() => {
if (isAuthenticated) {
@ -279,6 +280,23 @@ export function LoginPage() {
navigate(nextPath, { replace: true })
}
async function handleOAuth(provider: 'google' | 'github' | 'orcid') {
setError('')
setOauthLoading(provider)
try {
const result = await login({ method: provider, email: '' })
if (result?.ok === false) {
setError(result.error || 'Unable to continue with the selected identity provider.')
setOauthLoading(null)
return
}
setOauthLoading(null)
} catch {
setError('Unable to continue with the selected identity provider.')
setOauthLoading(null)
}
}
return (
<>
<SiteMetadata
@ -306,18 +324,18 @@ export function LoginPage() {
</form>
<div className="provider-row">
{isGoogleOAuthEnabled() ? (
<button className="button button--ghost button--full" type="button" onClick={() => void login({ method: 'google', email: '' })}>
Continue with Google
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('google')}>
{oauthLoading === 'google' ? 'Redirecting to Google...' : 'Continue with Google'}
</button>
) : null}
{isGitHubOAuthEnabled() ? (
<button className="button button--ghost button--full" type="button" onClick={() => void login({ method: 'github', email: '' })}>
Continue with GitHub
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('github')}>
{oauthLoading === 'github' ? 'Redirecting to GitHub...' : 'Continue with GitHub'}
</button>
) : null}
{isOrcidOAuthEnabled() ? (
<button className="button button--ghost button--full" type="button" onClick={() => void login({ method: 'orcid', email: '' })}>
Continue with ORCID
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('orcid')}>
{oauthLoading === 'orcid' ? 'Redirecting to ORCID...' : 'Continue with ORCID'}
</button>
) : null}
</div>
@ -330,13 +348,14 @@ export function LoginPage() {
export function RegisterPage() {
const navigate = useNavigate()
const { register, isAuthenticated } = usePlatformAuth()
const { login, register, isAuthenticated } = usePlatformAuth()
const nextPath = useNextPath()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [name, setName] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [oauthLoading, setOauthLoading] = useState<'google' | 'github' | 'orcid' | null>(null)
useEffect(() => {
if (isAuthenticated) {
@ -357,6 +376,23 @@ export function RegisterPage() {
navigate(nextPath, { replace: true })
}
async function handleOAuth(provider: 'google' | 'github' | 'orcid') {
setError('')
setOauthLoading(provider)
try {
const result = await login({ method: provider, email: '' })
if (result?.ok === false) {
setError(result.error || 'Unable to continue with the selected identity provider.')
setOauthLoading(null)
return
}
setOauthLoading(null)
} catch {
setError('Unable to continue with the selected identity provider.')
setOauthLoading(null)
}
}
return (
<>
<SiteMetadata
@ -384,6 +420,23 @@ export function RegisterPage() {
{isSubmitting ? 'Creating account...' : 'Create account'}
</button>
</form>
<div className="provider-row">
{isGoogleOAuthEnabled() ? (
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('google')}>
{oauthLoading === 'google' ? 'Redirecting to Google...' : 'Continue with Google'}
</button>
) : null}
{isGitHubOAuthEnabled() ? (
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('github')}>
{oauthLoading === 'github' ? 'Redirecting to GitHub...' : 'Continue with GitHub'}
</button>
) : null}
{isOrcidOAuthEnabled() ? (
<button className="button button--ghost button--full" disabled={oauthLoading !== null} type="button" onClick={() => void handleOAuth('orcid')}>
{oauthLoading === 'orcid' ? 'Redirecting to ORCID...' : 'Continue with ORCID'}
</button>
) : null}
</div>
<AuthBrowserDesktopRealityPanel />
<AuthAccessGuide nextPath={nextPath} />
</AuthShell>

View file

@ -3,6 +3,7 @@ import { ArrowRight, BookOpenText, Boxes, Download, ExternalLink, Landmark, Moni
import { Link, useSearchParams } from 'react-router-dom'
import { MarketingShell } from '../components/layout/MarketingShell'
import { SiteMetadata } from '../components/seo/SiteMetadata'
import { BrowserAuthMethodsGuide } from '../components/ui/BrowserAuthMethodsGuide'
import { PublicLaunchStatus } from '../components/ui/PublicLaunchStatus'
import { ProductSurfaceMatrix } from '../components/ui/ProductSurfaceMatrix'
import { brandConfig } from '../site-config'
@ -871,6 +872,13 @@ export function DocsPage() {
cards={operatorManualTracks}
/>
<Section
title="Browser account access methods"
description="The public manual now makes the current shared-auth lineup explicit too, so operators do not have to infer provider truth only from the sign-in form buttons."
>
<BrowserAuthMethodsGuide />
</Section>
<StepCardSection
title="Recovery and degraded-state manual"
description="This manual section explains how to move through auth or release-authority degradation without confusing fallback continuity for live production authority."

View file

@ -824,6 +824,21 @@ export const resourceCollections = [
] as const
export const changelogEntries = [
{
date: 'June 27, 2026',
title: 'Shared browser auth now exposes real provider parity and public-manual account guidance',
details: 'HyperTwist no longer leaves ORCID as a UI-only placeholder: the auth server now owns a bounded custom ORCID provider, same-origin bundle rendering emits matching ORCID env flags, auth-health reports ORCID readiness, login and register both recover cleanly around provider redirects, and the public docs route now shows the real browser-account method lineup instead of hiding it inside the form pages alone.',
},
{
date: 'June 27, 2026',
title: 'Shared browser auth now stays off the public first-paint path until it is actually needed',
details: 'Config-only auth runtime facts now live separately from the heavy SuperTokens client modules, the route wrapper is lazy-loaded only on auth-sensitive paths, and sign-in or sign-up or provider actions now import their recipe code on demand. The website production build no longer emits the oversized auth-core warning: the remaining auth core chunk is now 391.18 kB instead of staying above the Vite warning threshold.',
},
{
date: 'June 27, 2026',
title: 'Website startup now boots with centered first-paint shell and early shared-auth init',
details: 'The website entrypoint now initializes the shared SuperTokens posture before React root creation, and the public root document now ships a centered HyperTwist first-paint shell inside #root so public and protected routes no longer begin from a blank page while the first bundle loads.',
},
{
date: 'June 27, 2026',
title: 'The remaining legal public routes now carry the same release-decision guide too',
@ -1011,6 +1026,10 @@ export const sourceAvailability = {
}
export const supportFaqs = [
{
question: 'Which browser sign-in methods are actually supported?',
answer: 'Email/password is the baseline shared-auth lane. GitHub, Google, and ORCID may also appear when the current deployment has those providers configured. Regardless of method, browser sign-in only opens the protected account, release, and desktop-pairing surfaces; it does not replace the native desktop simulator.',
},
{
question: 'Is the simulator fully in the browser?',
answer: 'No. The current shipping lane is desktop-first and Unreal-backed. The public website offers account, operator, support, and download access, while the optional full-browser simulator path remains spec-only, so the browser does not currently replace the package-validated native runtime.',

View file

@ -7,8 +7,17 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('supertokens-auth-react/recipe/thirdparty')) {
return 'auth-thirdparty-vendor'
}
if (id.includes('supertokens-auth-react/recipe/emailpassword')) {
return 'auth-email-vendor'
}
if (id.includes('supertokens-auth-react/recipe/session')) {
return 'auth-session-vendor'
}
if (id.includes('supertokens-auth-react')) {
return 'auth-vendor'
return 'auth-core-vendor'
}
if (id.includes('react-router-dom') || id.includes('@tanstack/react-query')) {
return 'app-vendor'