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 87a2afc..649e58b 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 @@ -151,6 +151,7 @@ The repo bootstrap CI now also validates the website lane directly through: The frontend behavior coverage now also explicitly pins: - login redirect preservation for pathname, query, and hash deep links +- safe `next`-path normalization across custom auth pages and SuperTokens post-auth redirect handoff - browser auth-bootstrap normalization when the account payload reports email/fallback posture - dashboard launch-readiness visibility plus generated desktop-link verify URL behavior 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 e815d37..2d49ab3 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. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place, bootstrap CI now validates both the frontend and auth-server website commands directly, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin public deployment. | -| 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. Focused frontend coverage now also protects deep-link login redirect preservation, fallback/email auth-bootstrap normalization, and desktop-link verify-url/dashboard readiness behavior. | +| 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. Focused frontend coverage now also protects deep-link login redirect preservation, safe `next`-path normalization across auth entry points, fallback/email auth-bootstrap normalization, and desktop-link verify-url/dashboard readiness behavior. | | 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 65cbe7e..c2bcc5c 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -219,9 +219,10 @@ Current consolidated milestone snapshot: versus production example env families whose `replace-me` scaffolding is now explicitly rejected by that verifier, and the bootstrap CI lane now also runs the website/frontend plus website/server validation commands directly, while - focused frontend coverage now also pins deep-link login redirects, fallback - auth-bootstrap normalization, and dashboard launch-readiness plus desktop-link - verify-url behavior, and the auth server can now auto-serve the built + focused frontend coverage now also pins deep-link login redirects, safe + `next`-path normalization, fallback auth-bootstrap normalization, and + dashboard launch-readiness plus desktop-link verify-url behavior, and the + auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin `hypertwist.app` deployment when that build output is present, while the env templates and runtime-readiness verifier now also make that static-serving posture explicit diff --git a/website/README.md b/website/README.md index 30ad686..aade219 100644 --- a/website/README.md +++ b/website/README.md @@ -92,5 +92,6 @@ The repo bootstrap CI now also validates this lane through: The focused frontend test coverage now also pins: - route-guard redirect preservation for pathname, query, and hash deep links +- safe `next`-path normalization across custom auth pages and SuperTokens redirect handoff - auth-bootstrap normalization when fallback/email sessions are re-hydrated - dashboard launch-readiness plus desktop-link verify-url behavior diff --git a/website/src/__tests__/next-path.test.ts b/website/src/__tests__/next-path.test.ts new file mode 100644 index 0000000..e956481 --- /dev/null +++ b/website/src/__tests__/next-path.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { normalizeNextPath } from '../auth/next-path' + +describe('normalizeNextPath', () => { + it('keeps safe in-app next paths, including query and hash segments', () => { + expect(normalizeNextPath('/app/downloads?platform=windows#desktop-link')).toBe('/app/downloads?platform=windows#desktop-link') + }) + + it('falls back when next is missing, external, or protocol-relative', () => { + expect(normalizeNextPath('', '/app')).toBe('/app') + expect(normalizeNextPath(null, '/app')).toBe('/app') + expect(normalizeNextPath('https://evil.example', '/app')).toBe('/app') + expect(normalizeNextPath('//evil.example', '/app')).toBe('/app') + }) + + it('supports an empty-string fallback for call sites that prefer nullish handling', () => { + expect(normalizeNextPath('//evil.example', '')).toBe('') + }) +}) diff --git a/website/src/auth/next-path.ts b/website/src/auth/next-path.ts new file mode 100644 index 0000000..18bb38c --- /dev/null +++ b/website/src/auth/next-path.ts @@ -0,0 +1,7 @@ +export function normalizeNextPath(value: string | null | undefined, fallback = '/app') { + const next = String(value || '').trim() + if (!next || !next.startsWith('/') || next.startsWith('//')) { + return fallback + } + return next +} diff --git a/website/src/auth/supertokens-client.ts b/website/src/auth/supertokens-client.ts index 44b935c..158f6dd 100644 --- a/website/src/auth/supertokens-client.ts +++ b/website/src/auth/supertokens-client.ts @@ -3,6 +3,7 @@ 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() @@ -21,11 +22,7 @@ let initialized = false function readRequestedPostAuthPath() { if (typeof window === 'undefined') return null const params = new URLSearchParams(window.location.search) - const requestedPath = String(params.get('next') || '').trim() - if (!requestedPath || !requestedPath.startsWith('/') || requestedPath.startsWith('//')) { - return null - } - return requestedPath + return normalizeNextPath(params.get('next'), '') || null } const SUPERTOKENS_BRAND_STYLE = ` diff --git a/website/src/pages/auth-pages.tsx b/website/src/pages/auth-pages.tsx index ba360a3..8984df9 100644 --- a/website/src/pages/auth-pages.tsx +++ b/website/src/pages/auth-pages.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { usePlatformAuth } from '../auth/platform-auth' +import { normalizeNextPath } from '../auth/next-path' import { getSuperTokensAuthRuntimeValidation, isGitHubOAuthEnabled, @@ -36,8 +37,7 @@ function AuthShell({ function useNextPath() { const [searchParams] = useSearchParams() - const next = searchParams.get('next') || '/app' - return next.startsWith('/') ? next : '/app' + return normalizeNextPath(searchParams.get('next')) } export function LoginPage() {