Add website runtime deployment diagnostics

This commit is contained in:
axiomlogicnexus 2026-06-22 02:30:46 +00:00
parent 88e795abd3
commit 126768ee3a
15 changed files with 523 additions and 5 deletions

View file

@ -92,6 +92,7 @@ Current behavior:
- optional ORCID redirect posture retained client-side as a backend-gated extension point
- 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
### Browser dashboard posture
@ -118,6 +119,11 @@ The protected dashboard now also has a first-party launch-readiness panel for:
- Paddle checkout-link presence by plan
- corresponding-source and open-source notices URL presence
- backend webhook-secret and billing-map configuration posture
- public auth runtime posture, cookie-hardening posture, and local-vs-public runtime diagnostics
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.
This is browser-based user access for the operator/account surface.
@ -188,6 +194,11 @@ Before public launch, operators still must configure:
- live notification-destination secret management
- any broader operator/admin billing workflow beyond the current bounded entitlement application
Recommended public runtime values and same-origin deployment posture now have a
dedicated authority note:
- `docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md`
## Validation completed
Frontend validation:

View file

@ -0,0 +1,90 @@
# HyperTwist Website Runtime Configuration Guide
Created on `2026-06-22`.
## Purpose
This note records the recommended production runtime posture for the first-party
`website/` and `website/server/` lane that powers `hypertwist.app`.
It exists because the source tree now has:
- a real public website
- a real browser auth/dashboard surface
- explicit runtime diagnostics that can distinguish local, mixed, and public posture
## Recommended public posture
For the simplest honest public deployment, prefer same-origin browser and auth
posture:
- frontend public origin: `https://hypertwist.app`
- backend public origin: `https://hypertwist.app`
- frontend route prefix: `/`
- auth API base path: `/auth`
- auth website base path: `/auth`
Recommended server env posture:
- `API_DOMAIN=https://hypertwist.app`
- `WEBSITE_DOMAIN=https://hypertwist.app`
- `API_BASE_PATH=/auth`
- `WEBSITE_BASE_PATH=/auth`
- `COOKIE_SECURE=true`
- real `PADDLE_WEBHOOK_SECRET`
- real `PADDLE_PRODUCT_PLAN_MAP` and/or `PADDLE_PRICE_PLAN_MAP`
Recommended frontend env posture:
- `VITE_SUPERTOKENS_API_DOMAIN=https://hypertwist.app`
- `VITE_SUPERTOKENS_WEBSITE_DOMAIN=https://hypertwist.app`
- `VITE_AUTH_API_BASE_URL=https://hypertwist.app`
- real `VITE_WINDOWS_DOWNLOAD_URL`
- real `VITE_PADDLE_CHECKOUT_URL_OPERATOR`
- real `VITE_PADDLE_CHECKOUT_URL_STUDIO`
- real `VITE_MPL_SOURCE_URL`
- real `VITE_OPEN_SOURCE_REPO_URL`
## Why same-origin is the clean default
The current first-party browser lane already supports exact-origin checks,
dashboard-side launch-readiness visibility, and same-origin auth fallback.
That means same-origin public deployment is the least ambiguous posture for:
- auth cookies
- protected dashboard access
- browser-to-desktop token issuance
- public download gating
- billing webhook and entitlement reflection
## If a split-host auth topology is chosen later
That is allowed, but it should not be the unexamined default.
If a split-host topology is used:
- keep all public-facing auth origins on `https`
- keep `COOKIE_SECURE=true`
- ensure `VITE_AUTH_API_BASE_URL` and `VITE_SUPERTOKENS_API_DOMAIN` still point at the same backend
- ensure dashboard health reports `public auth origin ready: yes`
- ensure launch-readiness warnings are empty before public launch
## Current diagnostics that must be green
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
- dashboard launch-readiness issues cleared for download/check-out/source configuration
## Related authorities
- `website/README.md`
- `website/server/README.md`
- `docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md`
- `docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md`
- `docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md`
- `docs/ops/HYPERTWIST_VPS_TLS_CERTIFICATE_AND_IONOS_SEPARATION_GUIDE_2026-05-30.md`

View file

@ -264,7 +264,7 @@ repo.
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
| Public `hypertwist.app` marketing shell | Implemented now | first-party `website/` app + feature registry/roadmap authority | HyperTwist now has a dedicated first-party public web surface for homepage, about, resources, pricing, download, support, and legal routes. This lane is separate from the embedded Unreal browser runtime under `Content/Browser/` and does not claim browser-simulator parity. |
| Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, and billing-secret/map configuration. |
| Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, billing-secret/map configuration, and local-versus-public runtime deployment posture. |
| Desktop download posture and browser-to-desktop pairing | Implemented now | first-party `website/` app + `website/server` desktop-link endpoints | Public download targets, dashboard-side release posture, and short-lived desktop-link token generation/verification are now first-party owned. The current server posture now enforces exact website-origin matching, bounded per-user issuance, one-time token consumption, and billing-backed plan/download entitlement resolution with focused `website/server` tests green on `2026-06-22`, and the verify handshake now returns the same resolved download-entitlement posture the dashboard sees instead of only identity plus plan/role. The public `/download` page now keeps raw download URLs behind the protected dashboard instead of exposing them directly. Actual release URLs remain deployment configuration rather than hardcoded product truth. |
| Paddle-ready pricing and billing webhook seam | Implemented now | first-party `website/` app + `website/server` billing endpoint | The public pricing surface now exists with plan structure, checkout-link configuration seams, and the same `/api/billing/paddle/webhook` endpoint family used by the broader product website lane. The current server now verifies `Paddle-Signature` against `PADDLE_WEBHOOK_SECRET` using the documented raw-body HMAC flow, persists a bounded first-party billing state file, and applies verified Paddle events into account/download entitlement state that the browser dashboard consumes, with focused `website/server` tests green on `2026-06-22`. Production checkout URLs, secret management, and broader operator/admin billing workflows remain deployment/application tasks, not shipped-code omissions. |
| Public open-source notices and corresponding-source surface | Implemented now | first-party `website/` app + `HYPERTWIST_MPL_DISTRIBUTION_PLACEMENT_CHECKLIST_2026-05-25.md` | HyperTwist now has a stable public `Open Source Notices` route linked from pricing, download, and footer surfaces, satisfying the requirement that public distribution surfaces expose notice and corresponding-source guidance when shipped builds contain `MPL`-covered material. The exact public corresponding-source URL still must be configured before external launch. |

View file

@ -209,7 +209,11 @@ Current consolidated milestone snapshot:
fallback-active reason back into the protected dashboard rather than
hardcoding readiness, and the protected dashboard now also carries a
first-party launch-readiness panel for download/check-out/legal configuration
posture using the existing site-config and auth-health seams,
posture using the existing site-config and auth-health seams, while the
website/server lane now also has first-party runtime-configuration
diagnostics for local versus mixed versus public auth posture and the
login/register surfaces now expose those warnings instead of silently
resembling a production-ready lane,
persists a bounded first-party billing-state file, applies verified Paddle
events into account/download entitlement state, and surfaces that resolved
billing/download posture back through `/api/auth/me`, the protected browser

View file

@ -3,6 +3,7 @@ VITE_SUPERTOKENS_WEBSITE_DOMAIN=http://localhost:4273
VITE_SUPERTOKENS_API_BASE_PATH=/auth
VITE_SUPERTOKENS_WEBSITE_BASE_PATH=/auth
VITE_AUTH_API_BASE_URL=http://localhost:3001
VITE_AUTH_API_TIMEOUT_MS=8000
VITE_GITHUB_OAUTH_ENABLED=false
VITE_GOOGLE_OAUTH_ENABLED=false
VITE_ORCID_OAUTH_ENABLED=false

View file

@ -8,6 +8,7 @@ First-party `hypertwist.app` surface for HyperTwist:
- desktop download posture and desktop-link handshake endpoints
- Paddle-ready pricing/check-out wiring
- dashboard-side launch-readiness surface for download, checkout, auth, and notice configuration
- runtime diagnostics that distinguish local, mixed, and public auth deployment posture
- public open-source notices surface required by HyperTwist's MPL distribution doctrine
## Why this app exists
@ -65,3 +66,7 @@ Before public launch, configure:
Do not launch the public pricing/download pages without a valid open-source
notices and corresponding-source destination.
Recommended production posture is documented in:
- `docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md`

View file

@ -11,6 +11,7 @@ Responsibilities:
- Paddle webhook endpoint with raw-body signature verification
- verified billing-event application into first-party account/download entitlement state
- public auth-health probe for the website with configured vs reachable core truth
- runtime configuration diagnostics for local vs mixed vs public auth posture
## Quick start
@ -61,3 +62,15 @@ The auth-health route now probes the configured SuperTokens core non-mutatively:
- `SUPERTOKENS_HEALTH_TIMEOUT_MS` bounds the probe duration
- `/api/auth/health` now distinguishes configured, reachable, and ready state
- the protected dashboard can now show when browser fallback posture is active because the shared auth core is unreachable
Recommended public `hypertwist.app` posture:
- `API_DOMAIN=https://hypertwist.app`
- `WEBSITE_DOMAIN=https://hypertwist.app`
- `COOKIE_SECURE=true`
- real `PADDLE_WEBHOOK_SECRET`
- real plan-map configuration or equivalent verified-event plan resolution
That recommended same-origin production posture is recorded in:
- `docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md`

View file

@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import { getRuntimeConfigDiagnostics } from '../runtime-config'
describe('getRuntimeConfigDiagnostics', () => {
it('marks loopback-only development posture as local and not public-ready', () => {
const diagnostics = getRuntimeConfigDiagnostics({
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:4273',
superTokensCoreUri: 'http://localhost:3567',
cookieSecure: false,
})
expect(diagnostics.mode).toBe('local')
expect(diagnostics.publicOriginReady).toBe(false)
expect(diagnostics.errors).toEqual([])
expect(diagnostics.warnings).toContain('API_DOMAIN still targets a loopback/local-development origin.')
expect(diagnostics.warnings).toContain('WEBSITE_DOMAIN still targets a loopback/local-development origin.')
})
it('marks public https posture as ready when cookie hardening is enabled', () => {
const diagnostics = getRuntimeConfigDiagnostics({
apiDomain: 'https://hypertwist.app',
websiteDomain: 'https://hypertwist.app',
superTokensCoreUri: 'https://auth-core.internal',
cookieSecure: true,
githubClientId: 'github-client-id',
githubClientSecret: 'github-client-secret',
})
expect(diagnostics.mode).toBe('public')
expect(diagnostics.publicOriginReady).toBe(true)
expect(diagnostics.errors).toEqual([])
expect(diagnostics.warnings).toEqual([])
})
it('flags invalid origins and partial oauth configuration', () => {
const diagnostics = getRuntimeConfigDiagnostics({
apiDomain: 'https://hypertwist.app/auth',
websiteDomain: 'not-a-url',
superTokensCoreUri: 'http://localhost:3567',
cookieSecure: false,
googleClientId: 'only-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.')
})
})

View file

@ -13,6 +13,7 @@ import GoogleProvider from 'supertokens-node/lib/build/recipe/thirdparty/provide
import { probeSuperTokensCoreHealth } from './auth-health'
import { createBillingStateStore, type BillingPlan, type BillingRole } from './billing-state'
import { verifyPaddleWebhookSignature } from './paddle-webhook'
import { getRuntimeConfigDiagnostics } from './runtime-config'
import { buildAllowedOriginMatcher, createDesktopLinkStore } from './security'
const Github = GithubProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GithubProvider>
@ -41,6 +42,16 @@ const PADDLE_WEBHOOK_TOLERANCE_MS = Number(process.env.PADDLE_WEBHOOK_TOLERANCE_
const BILLING_STATE_PATH = process.env.BILLING_STATE_PATH || path.join(process.cwd(), 'data', 'hypertwist-billing-state.json')
const PADDLE_PRODUCT_PLAN_MAP = parseBillingPlanMap(process.env.PADDLE_PRODUCT_PLAN_MAP)
const PADDLE_PRICE_PLAN_MAP = parseBillingPlanMap(process.env.PADDLE_PRICE_PLAN_MAP)
const runtimeConfigDiagnostics = getRuntimeConfigDiagnostics({
apiDomain: API_DOMAIN,
websiteDomain: WEBSITE_DOMAIN,
superTokensCoreUri: SUPERTOKENS_CORE_URI,
cookieSecure: COOKIE_SECURE,
githubClientId: GITHUB_CLIENT_ID,
githubClientSecret: GITHUB_CLIENT_SECRET,
googleClientId: GOOGLE_CLIENT_ID,
googleClientSecret: GOOGLE_CLIENT_SECRET,
})
function normalizeBillingPlan(value: string): BillingPlan {
const plan = value.trim().toLowerCase()
@ -298,6 +309,15 @@ app.get('/api/auth/health', async (_req, res) => {
productPlanMapConfigured: Object.keys(PADDLE_PRODUCT_PLAN_MAP).length > 0,
webhookSecretConfigured: Boolean(PADDLE_WEBHOOK_SECRET),
},
runtime: {
mode: runtimeConfigDiagnostics.mode,
public_origin_ready: runtimeConfigDiagnostics.publicOriginReady,
cookie_secure: runtimeConfigDiagnostics.cookieSecure,
api_domain: runtimeConfigDiagnostics.apiDomain,
website_domain: runtimeConfigDiagnostics.websiteDomain,
warnings: runtimeConfigDiagnostics.warnings,
errors: runtimeConfigDiagnostics.errors,
},
})
})
@ -406,4 +426,7 @@ app.listen(PORT, () => {
console.log(`HyperTwist auth server listening on ${API_DOMAIN}`)
console.log(`Frontend origin: ${WEBSITE_DOMAIN}`)
console.log(`SuperTokens core: ${SUPERTOKENS_CORE_URI}`)
if (runtimeConfigDiagnostics.errors.length > 0 || runtimeConfigDiagnostics.warnings.length > 0) {
console.warn('[hypertwist-auth-server] runtime configuration diagnostics', runtimeConfigDiagnostics)
}
})

View file

@ -0,0 +1,160 @@
interface RuntimeConfigInput {
apiDomain: string
websiteDomain: string
superTokensCoreUri: string
cookieSecure: boolean
githubClientId?: string
githubClientSecret?: string
googleClientId?: string
googleClientSecret?: string
}
export interface RuntimeConfigDiagnostics {
mode: 'local' | 'mixed' | 'public'
publicOriginReady: boolean
cookieSecure: boolean
warnings: string[]
errors: string[]
apiDomain: string
websiteDomain: string
}
function normalizeTrimmed(value: string) {
return value.trim()
}
function tryParseUrl(value: string) {
const trimmed = normalizeTrimmed(value)
if (!trimmed) {
return null
}
try {
return new URL(trimmed)
} catch {
return null
}
}
function hasOriginOnlyPath(url: URL) {
return url.pathname === '/' || url.pathname === ''
}
function isLoopbackHostname(hostname: string) {
const normalized = hostname.trim().toLowerCase()
return normalized === 'localhost'
|| normalized === '127.0.0.1'
|| normalized === '::1'
|| normalized === '[::1]'
}
function isHttpsUrl(url: URL) {
return url.protocol === 'https:'
}
function hasCompleteCredentialPair(valueA: string | undefined, valueB: string | undefined) {
return Boolean(String(valueA || '').trim() && String(valueB || '').trim())
}
export function getRuntimeConfigDiagnostics({
apiDomain,
websiteDomain,
superTokensCoreUri,
cookieSecure,
githubClientId,
githubClientSecret,
googleClientId,
googleClientSecret,
}: RuntimeConfigInput): RuntimeConfigDiagnostics {
const warnings: string[] = []
const errors: string[] = []
const parsedApiDomain = tryParseUrl(apiDomain)
const parsedWebsiteDomain = tryParseUrl(websiteDomain)
const parsedSuperTokensCoreUri = tryParseUrl(superTokensCoreUri)
if (!parsedApiDomain) {
errors.push('API_DOMAIN must be an absolute URL origin.')
} else if (!hasOriginOnlyPath(parsedApiDomain)) {
errors.push('API_DOMAIN must not include a path; use API_BASE_PATH for route prefixing.')
}
if (!parsedWebsiteDomain) {
errors.push('WEBSITE_DOMAIN must be an absolute URL origin.')
} else if (!hasOriginOnlyPath(parsedWebsiteDomain)) {
errors.push('WEBSITE_DOMAIN must not include a path; use WEBSITE_BASE_PATH for route prefixing.')
}
if (!parsedSuperTokensCoreUri) {
errors.push('SUPERTOKENS_CORE_URI must be an absolute URL.')
}
const apiIsLoopback = parsedApiDomain ? isLoopbackHostname(parsedApiDomain.hostname) : false
const websiteIsLoopback = parsedWebsiteDomain ? isLoopbackHostname(parsedWebsiteDomain.hostname) : false
const coreIsLoopback = parsedSuperTokensCoreUri ? isLoopbackHostname(parsedSuperTokensCoreUri.hostname) : false
const allLoopback = apiIsLoopback && websiteIsLoopback && coreIsLoopback
const noLoopback = !apiIsLoopback && !websiteIsLoopback && !coreIsLoopback
if (apiIsLoopback) {
warnings.push('API_DOMAIN still targets a loopback/local-development origin.')
}
if (websiteIsLoopback) {
warnings.push('WEBSITE_DOMAIN still targets a loopback/local-development origin.')
}
if (coreIsLoopback) {
warnings.push('SUPERTOKENS_CORE_URI still targets a loopback/local-development host.')
}
if (parsedApiDomain && !apiIsLoopback && !isHttpsUrl(parsedApiDomain)) {
warnings.push('API_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
}
if (parsedWebsiteDomain && !websiteIsLoopback && !isHttpsUrl(parsedWebsiteDomain)) {
warnings.push('WEBSITE_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
}
if (!cookieSecure && !allLoopback) {
warnings.push('COOKIE_SECURE is disabled; production auth cookies will not be hardened.')
}
if (cookieSecure && (
(parsedApiDomain && !apiIsLoopback && !isHttpsUrl(parsedApiDomain))
|| (parsedWebsiteDomain && !websiteIsLoopback && !isHttpsUrl(parsedWebsiteDomain))
)) {
warnings.push('COOKIE_SECURE is enabled while a non-loopback auth origin still uses HTTP; verify reverse-proxy TLS posture.')
}
if (!hasCompleteCredentialPair(githubClientId, githubClientSecret) && (githubClientId || githubClientSecret)) {
warnings.push('GitHub OAuth is only partially configured; set both client ID and client secret or neither.')
}
if (!hasCompleteCredentialPair(googleClientId, googleClientSecret) && (googleClientId || googleClientSecret)) {
warnings.push('Google OAuth is only partially configured; set both client ID and client secret or neither.')
}
const mode: RuntimeConfigDiagnostics['mode'] = allLoopback
? 'local'
: noLoopback
? 'public'
: 'mixed'
const publicOriginReady = Boolean(
parsedApiDomain
&& parsedWebsiteDomain
&& !apiIsLoopback
&& !websiteIsLoopback
&& isHttpsUrl(parsedApiDomain)
&& isHttpsUrl(parsedWebsiteDomain)
&& cookieSecure
&& errors.length === 0
)
return {
mode,
publicOriginReady,
cookieSecure,
warnings,
errors,
apiDomain: normalizeTrimmed(apiDomain),
websiteDomain: normalizeTrimmed(websiteDomain),
}
}

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { getAuthRuntimeConfig } from '../auth/auth-env'
import { getAuthRuntimeConfig, validateAuthRuntimeConfig } from '../auth/auth-env'
describe('getAuthRuntimeConfig', () => {
it('normalizes loopback origins to the current browser origin', () => {
@ -14,4 +14,29 @@ describe('getAuthRuntimeConfig', () => {
expect(config.superTokensWebsiteDomain).toBe('http://127.0.0.1:4273')
expect(config.authApiBaseUrl).toBe('http://localhost:3001')
})
it('warns when auth runtime still targets local development origins', () => {
const validation = validateAuthRuntimeConfig({
VITE_SUPERTOKENS_API_DOMAIN: 'http://localhost:3001',
VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'http://localhost:4273',
VITE_AUTH_API_BASE_URL: 'http://localhost:3001',
})
expect(validation.ready).toBe(true)
expect(validation.warnings).toContain('VITE_SUPERTOKENS_API_DOMAIN still targets a loopback/local-development origin.')
expect(validation.warnings).toContain('VITE_SUPERTOKENS_WEBSITE_DOMAIN still targets a loopback/local-development origin.')
expect(validation.warnings).toContain('VITE_AUTH_API_BASE_URL still targets a loopback/local-development origin.')
})
it('warns when public auth config still uses insecure http or split backends', () => {
const validation = validateAuthRuntimeConfig({
VITE_SUPERTOKENS_API_DOMAIN: 'http://api.hypertwist.app',
VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'http://hypertwist.app',
VITE_AUTH_API_BASE_URL: 'https://auth.hypertwist.app',
})
expect(validation.warnings).toContain('VITE_SUPERTOKENS_API_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
expect(validation.warnings).toContain('VITE_SUPERTOKENS_WEBSITE_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
expect(validation.warnings).toContain('VITE_AUTH_API_BASE_URL differs from VITE_SUPERTOKENS_API_DOMAIN; verify both point at the same auth backend.')
})
})

View file

@ -74,6 +74,15 @@ export interface AuthHealthPayload {
productPlanMapConfigured: boolean
webhookSecretConfigured: boolean
}
runtime: {
mode: 'local' | 'mixed' | 'public'
public_origin_ready: boolean
cookie_secure: boolean
api_domain: string
website_domain: string
warnings: string[]
errors: string[]
}
}
export interface ApiBootstrapUserPayload {

View file

@ -2,6 +2,24 @@ function normalizeEnvValue(value: unknown) {
return String(value || '').trim().replace(/\/$/, '')
}
function tryParseUrl(value: string) {
const trimmed = normalizeEnvValue(value)
if (!trimmed) return null
try {
return new URL(trimmed)
} catch {
return null
}
}
function isLoopbackHostname(hostname: string) {
const normalized = hostname.trim().toLowerCase()
return normalized === 'localhost'
|| normalized === '127.0.0.1'
|| normalized === '::1'
|| normalized === '[::1]'
}
function normalizeLoopbackOrigin(configuredOrigin: string, locationOrigin: string) {
if (!configuredOrigin || !locationOrigin) return configuredOrigin
@ -84,6 +102,34 @@ export function validateAuthRuntimeConfig(
warnings.push('VITE_AUTH_API_BASE_URL not configured; same-origin auth fallback remains active.')
}
const apiUrl = tryParseUrl(config.superTokensApiDomain)
const websiteUrl = tryParseUrl(config.superTokensWebsiteDomain)
const authApiUrl = tryParseUrl(config.authApiBaseUrl)
if (apiUrl && isLoopbackHostname(apiUrl.hostname)) {
warnings.push('VITE_SUPERTOKENS_API_DOMAIN still targets a loopback/local-development origin.')
}
if (websiteUrl && isLoopbackHostname(websiteUrl.hostname)) {
warnings.push('VITE_SUPERTOKENS_WEBSITE_DOMAIN still targets a loopback/local-development origin.')
}
if (authApiUrl && isLoopbackHostname(authApiUrl.hostname)) {
warnings.push('VITE_AUTH_API_BASE_URL still targets a loopback/local-development origin.')
}
if (apiUrl && !isLoopbackHostname(apiUrl.hostname) && apiUrl.protocol !== 'https:') {
warnings.push('VITE_SUPERTOKENS_API_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
}
if (websiteUrl && !isLoopbackHostname(websiteUrl.hostname) && websiteUrl.protocol !== 'https:') {
warnings.push('VITE_SUPERTOKENS_WEBSITE_DOMAIN uses HTTP on a non-loopback origin; use HTTPS before public launch.')
}
if (authApiUrl && !isLoopbackHostname(authApiUrl.hostname) && authApiUrl.protocol !== 'https:') {
warnings.push('VITE_AUTH_API_BASE_URL uses HTTP on a non-loopback origin; use HTTPS before public launch.')
}
if (config.authApiBaseUrl && config.superTokensApiDomain && config.authApiBaseUrl !== config.superTokensApiDomain) {
warnings.push('VITE_AUTH_API_BASE_URL differs from VITE_SUPERTOKENS_API_DOMAIN; verify both point at the same auth backend.')
}
return {
ready: missing.length === 0,
missing,

View file

@ -24,7 +24,7 @@ function Panel({
}
export function DashboardOverviewPage() {
const { user } = usePlatformAuth()
const { user, superTokensConfigured } = usePlatformAuth()
const healthQuery = useQuery({
queryKey: ['auth-health'],
queryFn: getAuthHealth,
@ -49,6 +49,7 @@ export function DashboardOverviewPage() {
if (!launchReadiness.mplSourceConfigured) issues.push('MPL corresponding-source URL missing')
if (!launchReadiness.openSourceRepoConfigured) issues.push('Open-source repository/notices URL missing')
if (healthQuery.data && !healthQuery.data.billing.webhookSecretConfigured) issues.push('Paddle webhook secret missing')
if (healthQuery.data && !healthQuery.data.runtime.public_origin_ready) issues.push('Public auth runtime still uses local or mixed deployment posture')
return issues
}, [healthQuery.data])
@ -60,6 +61,11 @@ export function DashboardOverviewPage() {
<p>Plan: {user?.plan}</p>
<p>Role: {user?.role || 'operator'}</p>
<p>Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}</p>
{!superTokensConfigured || user?.billing?.source === 'local-fallback' ? (
<p className="form-error">
This session is currently using local fallback posture, not fully shared production auth.
</p>
) : null}
</Panel>
<Panel title="Auth and server health" kicker="Browser shell">
@ -75,6 +81,9 @@ export function DashboardOverviewPage() {
<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>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>
</ul>
) : null}
{healthQuery.data?.fallback.active ? (
@ -82,6 +91,25 @@ export function DashboardOverviewPage() {
Shared auth core is not fully ready right now. Dashboard fallback posture remains available{healthQuery.data.fallback.reason ? ` (${healthQuery.data.fallback.reason})` : ''}.
</p>
) : null}
{healthQuery.data && (healthQuery.data.runtime.errors.length > 0 || healthQuery.data.runtime.warnings.length > 0) ? (
<div className="callout">
<p>Runtime deployment diagnostics</p>
{healthQuery.data.runtime.errors.length > 0 ? (
<ul className="list">
{healthQuery.data.runtime.errors.map((item) => (
<li key={item}>Error: {item}</li>
))}
</ul>
) : null}
{healthQuery.data.runtime.warnings.length > 0 ? (
<ul className="list">
{healthQuery.data.runtime.warnings.map((item) => (
<li key={item}>Warning: {item}</li>
))}
</ul>
) : null}
</div>
) : null}
</Panel>
<Panel title="Desktop-link pairing" kicker="Browser to desktop handoff">
@ -127,6 +155,7 @@ export function DashboardOverviewPage() {
<li>Paddle webhook secret: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.webhookSecretConfigured ? 'configured' : 'missing')}</li>
<li>Billing product map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.productPlanMapConfigured ? 'configured' : 'missing')}</li>
<li>Billing price map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.pricePlanMapConfigured ? 'configured' : 'missing')}</li>
<li>Public auth runtime posture: {!healthQuery.data ? 'checking' : (healthQuery.data.runtime.public_origin_ready ? 'production-ready' : 'local-or-mixed')}</li>
</ul>
{launchReadinessIssues.length > 0 ? (
<p className="form-error">

View file

@ -1,7 +1,14 @@
import { useEffect, useState } from 'react'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { usePlatformAuth } from '../auth/platform-auth'
import { isGitHubOAuthEnabled, isGoogleOAuthEnabled, isOrcidOAuthEnabled } from '../auth/supertokens-client'
import {
getSuperTokensAuthRuntimeValidation,
isGitHubOAuthEnabled,
isGoogleOAuthEnabled,
isOrcidOAuthEnabled,
} from '../auth/supertokens-client'
const authRuntimeValidation = getSuperTokensAuthRuntimeValidation()
function AuthShell({
title,
@ -67,6 +74,28 @@ export function LoginPage() {
subtitle="Use the same browser auth posture as FamiliarOS and ScriptoriumAI, then hand off to the desktop runtime when needed."
footer={<p>Need access? <Link to={`/register?next=${encodeURIComponent(nextPath)}`}>Create an account</Link>.</p>}
>
{!authRuntimeValidation.ready || authRuntimeValidation.warnings.length > 0 ? (
<div className="callout">
<p>
Shared browser auth is not fully in production posture yet.
{!authRuntimeValidation.ready ? ' Local fallback mode may activate until the required auth env vars are configured.' : ''}
</p>
{authRuntimeValidation.missing.length > 0 ? (
<ul className="list">
{authRuntimeValidation.missing.map((item) => (
<li key={item}>Missing auth env: {item}</li>
))}
</ul>
) : null}
{authRuntimeValidation.warnings.length > 0 ? (
<ul className="list">
{authRuntimeValidation.warnings.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : null}
</div>
) : null}
<form className="auth-form" onSubmit={handleSubmit}>
<label className="input-label" htmlFor="login-email">Email</label>
<input id="login-email" className="input" value={email} onChange={(event) => setEmail(event.target.value)} />
@ -133,6 +162,28 @@ export function RegisterPage() {
subtitle="This unlocks the browser dashboard, release posture, and desktop-link pairing for the simulator lane."
footer={<p>Already have access? <Link to={`/login?next=${encodeURIComponent(nextPath)}`}>Log in</Link>.</p>}
>
{!authRuntimeValidation.ready || authRuntimeValidation.warnings.length > 0 ? (
<div className="callout">
<p>
Shared browser auth is not fully in production posture yet.
{!authRuntimeValidation.ready ? ' Local fallback mode may activate until the required auth env vars are configured.' : ''}
</p>
{authRuntimeValidation.missing.length > 0 ? (
<ul className="list">
{authRuntimeValidation.missing.map((item) => (
<li key={item}>Missing auth env: {item}</li>
))}
</ul>
) : null}
{authRuntimeValidation.warnings.length > 0 ? (
<ul className="list">
{authRuntimeValidation.warnings.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : null}
</div>
) : null}
<form className="auth-form" onSubmit={handleSubmit}>
<label className="input-label" htmlFor="register-name">Name</label>
<input id="register-name" className="input" value={name} onChange={(event) => setName(event.target.value)} />