From 126768ee3a187a64484f7fcaee7fcad6db164def Mon Sep 17 00:00:00 2001
From: axiomlogicnexus
Date: Mon, 22 Jun 2026 02:30:46 +0000
Subject: [PATCH] Add website runtime deployment diagnostics
---
...LING_AND_DISTRIBUTION_PACKET_2026-06-22.md | 11 ++
..._RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md | 90 ++++++++++
.../HyperTwist/FEATURE_REGISTRY.md | 2 +-
.../HyperTwist/ROADMAP.md | 6 +-
website/.env.example | 1 +
website/README.md | 5 +
website/server/README.md | 13 ++
.../src/__tests__/runtime-config.test.ts | 51 ++++++
website/server/src/index.ts | 23 +++
website/server/src/runtime-config.ts | 160 ++++++++++++++++++
website/src/__tests__/auth-env.test.ts | 27 ++-
website/src/auth/auth-api.ts | 9 +
website/src/auth/auth-env.ts | 46 +++++
website/src/pages/app-pages.tsx | 31 +++-
website/src/pages/auth-pages.tsx | 53 +++++-
15 files changed, 523 insertions(+), 5 deletions(-)
create mode 100644 docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md
create mode 100644 website/server/src/__tests__/runtime-config.test.ts
create mode 100644 website/server/src/runtime-config.ts
diff --git a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md
index ef77038..850a3a9 100644
--- a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md
+++ b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md
@@ -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:
diff --git a/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md b/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md
new file mode 100644
index 0000000..67f28ff
--- /dev/null
+++ b/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md
@@ -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`
diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md
index 4972c76..e221384 100644
--- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md
+++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md
@@ -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. |
diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md
index 4f56023..c25c86e 100644
--- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md
+++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md
@@ -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
diff --git a/website/.env.example b/website/.env.example
index b868be0..c1a0e90 100644
--- a/website/.env.example
+++ b/website/.env.example
@@ -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
diff --git a/website/README.md b/website/README.md
index 7aae4e8..89b9b71 100644
--- a/website/README.md
+++ b/website/README.md
@@ -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`
diff --git a/website/server/README.md b/website/server/README.md
index 0cede81..54808e4 100644
--- a/website/server/README.md
+++ b/website/server/README.md
@@ -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`
diff --git a/website/server/src/__tests__/runtime-config.test.ts b/website/server/src/__tests__/runtime-config.test.ts
new file mode 100644
index 0000000..51748ad
--- /dev/null
+++ b/website/server/src/__tests__/runtime-config.test.ts
@@ -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.')
+ })
+})
diff --git a/website/server/src/index.ts b/website/server/src/index.ts
index 5e04b80..0b84f3f 100644
--- a/website/server/src/index.ts
+++ b/website/server/src/index.ts
@@ -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
@@ -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)
+ }
})
diff --git a/website/server/src/runtime-config.ts b/website/server/src/runtime-config.ts
new file mode 100644
index 0000000..39f1415
--- /dev/null
+++ b/website/server/src/runtime-config.ts
@@ -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),
+ }
+}
diff --git a/website/src/__tests__/auth-env.test.ts b/website/src/__tests__/auth-env.test.ts
index c73dac8..c083024 100644
--- a/website/src/__tests__/auth-env.test.ts
+++ b/website/src/__tests__/auth-env.test.ts
@@ -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.')
+ })
})
diff --git a/website/src/auth/auth-api.ts b/website/src/auth/auth-api.ts
index e5a6a68..9109d13 100644
--- a/website/src/auth/auth-api.ts
+++ b/website/src/auth/auth-api.ts
@@ -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 {
diff --git a/website/src/auth/auth-env.ts b/website/src/auth/auth-env.ts
index fd33fb4..949c099 100644
--- a/website/src/auth/auth-env.ts
+++ b/website/src/auth/auth-env.ts
@@ -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,
diff --git a/website/src/pages/app-pages.tsx b/website/src/pages/app-pages.tsx
index b7a688c..249c09a 100644
--- a/website/src/pages/app-pages.tsx
+++ b/website/src/pages/app-pages.tsx
@@ -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() {
Plan: {user?.plan}
Role: {user?.role || 'operator'}
Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}
+ {!superTokensConfigured || user?.billing?.source === 'local-fallback' ? (
+
+ This session is currently using local fallback posture, not fully shared production auth.
+
+ ) : null}
@@ -75,6 +81,9 @@ export function DashboardOverviewPage() {
Fallback active: {healthQuery.data.fallback.active ? 'yes' : 'no'}
Core API version: {healthQuery.data.supertokens.apiVersion || 'unavailable'}
OAuth routes: GitHub {healthQuery.data.supertokens.oauth?.github ? 'on' : 'off'}, Google {healthQuery.data.supertokens.oauth?.google ? 'on' : 'off'}
+ Runtime mode: {healthQuery.data.runtime.mode}
+ Cookie secure: {healthQuery.data.runtime.cookie_secure ? 'yes' : 'no'}
+ Public auth origin ready: {healthQuery.data.runtime.public_origin_ready ? 'yes' : 'no'}
) : 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})` : ''}.
) : null}
+ {healthQuery.data && (healthQuery.data.runtime.errors.length > 0 || healthQuery.data.runtime.warnings.length > 0) ? (
+
+
Runtime deployment diagnostics
+ {healthQuery.data.runtime.errors.length > 0 ? (
+
+ {healthQuery.data.runtime.errors.map((item) => (
+ Error: {item}
+ ))}
+
+ ) : null}
+ {healthQuery.data.runtime.warnings.length > 0 ? (
+
+ {healthQuery.data.runtime.warnings.map((item) => (
+ Warning: {item}
+ ))}
+
+ ) : null}
+
+ ) : null}
@@ -127,6 +155,7 @@ export function DashboardOverviewPage() {
Paddle webhook secret: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.webhookSecretConfigured ? 'configured' : 'missing')}
Billing product map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.productPlanMapConfigured ? 'configured' : 'missing')}
Billing price map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.pricePlanMapConfigured ? 'configured' : 'missing')}
+ Public auth runtime posture: {!healthQuery.data ? 'checking' : (healthQuery.data.runtime.public_origin_ready ? 'production-ready' : 'local-or-mixed')}
{launchReadinessIssues.length > 0 ? (
diff --git a/website/src/pages/auth-pages.tsx b/website/src/pages/auth-pages.tsx
index 9a19b8e..ba360a3 100644
--- a/website/src/pages/auth-pages.tsx
+++ b/website/src/pages/auth-pages.tsx
@@ -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={
Need access? Create an account.
}
>
+ {!authRuntimeValidation.ready || authRuntimeValidation.warnings.length > 0 ? (
+
+
+ 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.' : ''}
+
+ {authRuntimeValidation.missing.length > 0 ? (
+
+ {authRuntimeValidation.missing.map((item) => (
+ Missing auth env: {item}
+ ))}
+
+ ) : null}
+ {authRuntimeValidation.warnings.length > 0 ? (
+
+ {authRuntimeValidation.warnings.map((item) => (
+ {item}
+ ))}
+
+ ) : null}
+
+ ) : null}