Add website auth surfaces and package validation proof

This commit is contained in:
axiomlogicnexus 2026-06-22 02:18:59 +00:00
parent a2a005979f
commit 5bb8b31ef9
58 changed files with 12682 additions and 6 deletions

4
.gitignore vendored
View file

@ -7,6 +7,10 @@ __pycache__/
TODO.json
Content/Browser/dist/
Content/Browser/node_modules/
website/dist/
website/node_modules/
website/server/node_modules/
website/server/data/
# Sensitive or reference-only material.
docs/refs/

View file

@ -0,0 +1,93 @@
{
"reportVersion": "ht-higher-dimensional-package-validation/v1",
"generatedAtUtc": "2026-06-22T01:43:08.7625247Z",
"projectRoot": "C:/HyperTwist_worktrees/phase10validate",
"archiveDirectory": "C:/HyperTwist_worktrees/phase10validate_packaged_phase6c_higherdim",
"configuration": "Development",
"cookMaps": [
"/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining"
],
"smokeMaps": [
"/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining"
],
"additionalCookerOptions": [
"-DisablePlugins=MovieRenderPipeline",
"-SkipCookingEditorContent"
],
"cleanArchive": true,
"skipBuild": true,
"skipLaunch": false,
"authoringManifest": {
"path": "C:\\HyperTwist_worktrees\\phase10validate\\docs\\generated\\higher_dimensional_training_maps\\phase6c_dedicated_family_map_manifest.json",
"manifestId": "phase6c/dedicated-family-training-map-authoring",
"manifestVersion": "2026.06.18",
"authoredThroughGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode",
"validatedEntries": [
{
"mapKind": "magic120cell",
"familyKey": "magic120cell",
"mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap",
"mapHashMd5": "9d9b9301f14cdad8c263d2c57b5f2c7f",
"activationProfileId": "magic120cell-cleanroom-runtime-activation",
"runtimeModeId": "magic120cell-full-color-runtime-v1",
"projectionProfileId": "magic120cell-4d-projection-distance-v1",
"primaryPersistenceBoundaryId": "magic120cell-persistence-boundary"
},
{
"mapKind": "magiccube5d",
"familyKey": "magiccube5d",
"mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
"mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap",
"mapHashMd5": "15d2f7acfdce43402ad2fd291fc5c781",
"activationProfileId": "magiccube5d-cleanroom-runtime-activation",
"runtimeModeId": "magiccube5d-order3-runtime-v1",
"projectionProfileId": "magiccube5d-5d-projection-distance-v1",
"primaryPersistenceBoundaryId": "magiccube5d-persistence-boundary"
}
]
},
"result": "passed",
"packagedExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
"smokeReports": [
{
"reportVersion": "ht-higher-dimensional-package-smoke/v1",
"generatedAtUtc": "2026-06-22T01:45:12.6335785Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"smokeSeconds": 10,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"result": "passed",
"processId": 29436,
"processStopped": true,
"exitCode": null,
"error": null
},
{
"reportVersion": "ht-higher-dimensional-package-smoke/v1",
"generatedAtUtc": "2026-06-22T01:45:23.3808621Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
"smokeSeconds": 10,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"result": "passed",
"processId": 35616,
"processStopped": true,
"exitCode": null,
"error": null
}
],
"error": null
}

View file

@ -369,6 +369,7 @@ Closure read:
- `2026-06-18` Windows checkpoint: after that projection-deferral hardening rebuild with `Result: Succeeded` / UnrealBuildTool `Total execution time: 400.77 seconds`, targeted `UnrealEditor-Cmd` automation exported `C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\AutomationReports\Phase6C-DedicatedFamily-Optimized2\index.json` with all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, `totalDuration` `41.88494873046875`, and `LaunchExecution` duration `41.53509521484375`; the controller quiet interval fell again to `41.52 seconds` while retaining `**** TEST COMPLETE. EXIT CODE: 0 ****`
- `2026-06-18` recovery proof: current code now deterministically reauthors the dedicated-family `Magic120Cell` / `MagicCube5D` maps through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, stamps `AHyperTwistCoachDashboardGameMode` plus manifest-backed `PlayerStart` ownership tags for activation, host, launch, view-context, session, interactive-scene, scene-context, puzzle, runtime-mode, projection, persistence, and dedicated-family ownership, and records the refreshed owned-map hashes in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json`; the optional `AHyperTwistHigherDimensionalTrainingShellActor` remains available for manual/editor authoring, but the headless `UnrealEditor-Cmd` lane now intentionally skips spawning it because that path still crashes the commandlet with `EXCEPTION_INT_DIVIDE_BY_ZERO`
- `2026-06-18` Windows checkpoint: after patching the missing dedicated-anchor puzzle tag, the same isolated worktree `C:\HyperTwist_worktrees\phase10validate` reran `scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1` to `Higher-dimensional authored maps verified.`, then targeted `UnrealEditor-Cmd` automation exported `C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\AutomationReports\Phase6C-DedicatedMapOwnership\index.json`; the pulled local proof at `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json` now shows all `13` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, including `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`, with `**** TEST COMPLETE. EXIT CODE: 0 ****`
- `2026-06-22` Windows checkpoint: the same primary reverse-SSH `localhost:22022` lane then re-used isolated worktree `C:\HyperTwist_worktrees\phase10validate` for the maintained higher-dimensional package helper, ran `scripts\Invoke-HyperTwistHigherDimensionalPackage.ps1` in `-SkipBuild` mode against the existing target receipt, validated the manifest-backed `Magic120Cell` / `MagicCube5D` dedicated-family map hashes before cook, completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 118.53 s`, wrote archive output to `C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`, smoke-booted both `/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining`, and pulled aggregate proof into `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
- [x] Replace the current placeholder interactive-scene envelopes with full owned `120-cell` and `5D` simulation-state, projection, and persistence ownership in Unreal above the landed higher-dimensional scene-context catalog
- [x] Replace the current shared-map execution bridge with full owned dedicated-family interactive launch, map, and persistence surfaces for `120-cell` and `5D`

View file

@ -0,0 +1,216 @@
# HyperTwist Public Website, Auth, Billing, and Distribution Packet
Created on `2026-06-22`.
## Purpose
This packet records the first-party public `hypertwist.app` lane that now sits
beside the existing embedded Unreal browser runtime.
It closes one product-surface gap:
- HyperTwist now has a real public website and browser operator shell
It does **not** reopen any previously frozen topology branch:
- it does not claim the optional full-browser simulator is live
- it does not reopen the native `MagicTile` renderer-port branch
- it does not replace the embedded Unreal browser/CEF shipping posture
## Why this lane had to exist separately
HyperTwist already had a browser runtime under:
- `Content/Browser/`
That runtime is the embedded Unreal browser/CEF shell, not the public website,
pricing page, download page, auth shell, or account dashboard.
The public product/distribution surface now lives under:
- `website/`
## Cross-repo reference posture
This packet intentionally reused website structure and auth posture from:
- FamiliarOS website structure, public-page layout discipline, and centered loading spinner
- ScriptoriumAI browser dashboard and protected-app-shell posture
But the resulting code is now:
- first-party HyperTwist-owned
- rebranded to HyperTwist content and product truth
- constrained by HyperTwist roadmap and feature-registry authority
## Landed first-party surfaces
### Frontend app
- `website/src/App.tsx`
- `website/src/router/AppRouteTree.tsx`
- `website/src/router/PublicRoutes.tsx`
- `website/src/components/layout/MarketingShell.tsx`
- `website/src/components/layout/AppShell.tsx`
- `website/src/components/ui/Skeletons.tsx`
- `website/src/pages/public-pages.tsx`
- `website/src/pages/auth-pages.tsx`
- `website/src/pages/app-pages.tsx`
Current public/product routes now include:
- `/`
- `/about`
- `/resources`
- `/docs`
- `/support`
- `/changelog`
- `/pricing`
- `/download`
- `/open-source-notices`
- `/privacy`
- `/terms`
- `/shipping-payment`
- `/login`
- `/register`
- protected `/app` routes for browser-based operator access
### Auth posture
Shared auth posture now exists through:
- `website/src/auth/auth-env.ts`
- `website/src/auth/auth-api.ts`
- `website/src/auth/supertokens-client.ts`
- `website/src/auth/platform-auth.tsx`
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
- 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
### Browser dashboard posture
Protected browser dashboard routes now expose:
- account state
- auth/server health
- desktop-link token generation
- download/release posture
- billing and entitlement state
- browser-access boundary explanation
- notices review
That auth/server health surface now also reports:
- whether the shared SuperTokens core is merely configured or actually reachable
- the reported core API version when reachable
- whether fallback posture is currently active and why
This is browser-based user access for the operator/account surface.
It is **not** a claim that the simulator itself is now browser-owned.
### Desktop distribution and pairing posture
The website now includes:
- public download center
- plan-gated release posture
- browser-to-desktop token handoff
- protected-dashboard download gating above the public target-description page
Current pairing endpoint family:
- `POST /api/auth/desktop-link`
- `GET /api/auth/desktop-link/verify`
Current hardening on that lane now also includes:
- exact website-origin matching instead of prefix-only acceptance
- bounded per-user desktop-link issuance
- one-time verification-token consumption after the first successful desktop-side use
- billing-resolved plan/role plus download-entitlement posture carried into the desktop-link token rather than stale session defaults
- public `/download` messaging that routes real download access through the protected dashboard instead of exposing raw release URLs on the marketing page
### Billing posture
Pricing now exists as a public first-party page and is wired for:
- env-configured plan labels
- env-configured Paddle checkout URLs
- shared webhook endpoint family
Current billing endpoint:
- `POST /api/billing/paddle/webhook`
Current billing hardening now includes:
- raw-body signature verification against `PADDLE_WEBHOOK_SECRET`
- `Paddle-Signature` timestamp tolerance enforcement
- bounded first-party billing-state persistence
- verified-event application into account/download entitlement state
- focused backend tests for valid, malformed, expired, and mismatched signatures
## Public legal and MPL distribution posture
This packet explicitly satisfies the earlier doctrine requirement that public
distribution surfaces must expose legal/notices guidance when shipped builds
contain `MPL`-covered material.
The website now has:
- a public `Open Source Notices` route
- footer linkage
- pricing/download/legal linkage
- corresponding-source configuration slots
This packet does **not** pretend deployment is finished.
Before public launch, operators still must configure:
- the real public corresponding-source URL
- real Paddle checkout URLs
- real production download URLs
- live notification-destination secret management
- any broader operator/admin billing workflow beyond the current bounded entitlement application
## Validation completed
Frontend validation:
- `npm run type-check`
- `npm test`
- `npm run build`
Backend validation:
- `npm run type-check`
- `npm test`
Validation result on `2026-06-22`:
- frontend type-check passed
- frontend tests passed
- frontend production build passed
- backend type-check passed
- backend focused security tests passed
- `website/server` billing/entitlement tests passed
- the new bounded auth-health probe tests passed, and the protected dashboard type/build surface remained green after adopting the richer health contract
## Explicit non-claims
This packet does not claim any of the following:
- public browser simulator parity with the native Unreal runtime
- live production pricing values
- live production download URLs
- completed public corresponding-source hosting
Those are deployment/runtime configuration tasks, not missing ownership of the
public website lane itself.

View file

@ -391,6 +391,49 @@ Live follow-up on `2026-06-18` established these additional facts:
`Ignoring very large delta of 499.13 seconds`, so the quiet interval is now
known-good on this lane rather than a default corruption indicator
## Addendum - 2026-06-22 (higher-dimensional structured package-report proof)
Live follow-up on `2026-06-22` established these additional facts:
- the primary `localhost:22022` lane remained healthy for the current
higher-dimensional package-helper proof
- the validated route again used the isolated Windows tree
`C:\HyperTwist_worktrees\phase10validate`
- the maintained helper pair
`scripts\Invoke-HyperTwistHigherDimensionalPackage.ps1` and
`scripts\Launch-HyperTwistHigherDimensionalPackage.ps1` was narrow-synced
into that isolated worktree without forcing a broad reverse-sync
- the packaged-game target receipt was already present there, so the helper was
validated in `-SkipBuild` mode rather than spending time on an unrelated full
game rebuild for this script-only slice
- before `BuildCookRun`, the helper verified that the
`Magic120Cell` / `MagicCube5D` dedicated-family `.umap` files still matched
the owned hashes recorded in
`docs\generated\higher_dimensional_training_maps\phase6c_dedicated_family_map_manifest.json`
- `BuildCookRun` then succeeded from that same worktree with `ExitCode=0` and
`BuildCookRun time: 118.53 s`
- archive output was written to
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`
- packaged smoke launch succeeded for
`/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining`
- packaged smoke launch also succeeded for
`/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining`
- the maintained helper wrote aggregate report JSON to
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim\validation\higher-dimensional-package-validation-report.json`
- the same aggregate proof was then pulled back into the tracked repo at
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
Operational rules reinforced by this proof:
- for a script-only packaging helper slice on this lane, prefer a narrow file
sync plus `-SkipBuild` when the packaged-game target receipt already exists
- when a package helper validates manifest-backed map freshness before cook,
treat that manifest gate as part of the proof rather than as incidental log
noise
- when a remote packaged-validation archive becomes relied-on source-backed
evidence, pull the aggregate report back into the tracked repo before calling
the lane closed
## Addendum - 2026-06-13 (replay, leaderboard, and structured package-report proof)
Live follow-up on `2026-06-13` established these additional facts:

View file

@ -193,6 +193,24 @@ can legitimately trigger controller output such as
healthy, so that quiet interval is now an expected operational characteristic
of this slice rather than a default corruption signal.
Additional live proof on `2026-06-22` then closed the current maintained
higher-dimensional package-helper lane on that same primary reverse-SSH path:
the isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate`
already had the packaged-game target receipt, so the new helper
`scripts\Invoke-HyperTwistHigherDimensionalPackage.ps1` was validated in
`-SkipBuild` mode against the existing receipt, insisted on manifest-backed
`Magic120Cell` / `MagicCube5D` dedicated-family map hash parity before cook,
completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 118.53 s`,
wrote archive output to
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`,
smoke-launched both higher-dimensional dedicated-family training maps through
`scripts\Launch-HyperTwistHigherDimensionalPackage.ps1`, and pulled aggregate
proof into
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`.
This extends the doctrine from higher-dimensional activation/ownership proof
into current structured package/archive/launch proof on the same primary
reverse-SSH lane.
Further `2026-06-13` continuation proof on that same primary lane recovered
the classic-cube replay/leaderboard/package slice after a corrupted reverse
sync incident: a prior aborted broad tar sync left remote

View file

@ -259,7 +259,17 @@ repo.
| Local social challenge bundle | Implemented now | landed `kash/cubedesk` `Bound 5` | Live bounded slice. |
| Broader admin/report lane | Deep-source grounded retained | repo-local justification required | `Bound 6` is deferred and not currently justified for implementation. |
### 6. Hyper puzzle topology and non-Euclidean runtime
### 6. Public website, distribution, and account surfaces
| 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, and 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. |
| 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. |
### 7. Hyper puzzle topology and non-Euclidean runtime
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
@ -279,10 +289,10 @@ repo.
| Higher-dimensional initial shared-map launch and view-context execution bridge | Implemented now | landed `Phase 6C` continuation packet | First-party current code first bundled higher-dimensional launch surfaces and view-context surfaces for `Magic120Cell` and `MagicCube5D`, resolved those surfaces through the training runtime library plus the live training panel/session-actor caches, and let the training subsystem start the shared-map seed run, apply default selector posture, persist the generated-mode launch request, and start the owned generated run without widening into external-process ownership. After recovering an interrupted follow-up, the primary reverse-SSH `localhost:22022` Windows lane rebuilt that cleaned slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 124.81 seconds`, and passed all `8` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including `LaunchCatalog`, `ViewContextCatalog`, `LaunchExecution`, and `RunStateActivationResolution`; that initial bridge is now superseded by the later dedicated-family map ownership continuation. |
| Higher-dimensional explicit dedicated-host seam | Implemented now | landed `Phase 6C` host-surface continuation packet | First-party current code then recorded the preferred dedicated-family `Magic120Cell` / `MagicCube5D` host-map targets in an explicit higher-dimensional host-surface catalog while the slice still kept the shared training map as the interim effective runtime fallback, mirrored that resolved host seam through the training runtime library plus the panel/session caches, and threaded the host-surface id into the launch/view-context surfaces and generated-mode lineage. The primary reverse-SSH `localhost:22022` Windows lane rebuilt this widened slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 2858.02 seconds`, and passed all `9` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including the new `HostCatalog`; that interim fallback is now superseded by the later dedicated-family map ownership continuation. |
| Higher-dimensional executed runtime session surface | Implemented now | landed `Phase 6C` session-surface continuation packet | First-party current code now merges the higher-dimensional launch, host, and view-context seams into an explicit runtime-session catalog for `Magic120Cell` and `MagicCube5D`, mirrors that session surface through the training runtime library plus the panel/session caches, upgrades generated-mode deck lineage to record the resolved session surface, and applies the merged launch-plus-view selector posture at start time instead of only the narrower launch defaults. The primary reverse-SSH `localhost:22022` Windows lane rebuilt this widened slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 3541.98 seconds`, and passed all `10` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including the new `SessionCatalog`; the longer `LaunchExecution` quiet interval of `573.84 seconds` remained known-good. |
| Higher-dimensional dedicated-family interactive scene ownership | Implemented now | landed `Phase 6C` interactive-scene and dedicated-host continuation packet | First-party current code now promotes the higher-dimensional runtime-session seams into explicit `Magic120Cell` and `MagicCube5D` interactive-scene surfaces with family-owned runtime-state, projection, and persistence envelopes, replaces the shared-map bridge with dedicated-family training-map assets plus dedicated host/launch/view-context/session surfaces, exposes explicit scene-envelope decoders through the runtime library, batches default selector application so generated-mode launch execution no longer rebuilds repository views once per selector, and then defers repository-view projection across the internal generated-mode handoff itself so only the final launch state forces a view refresh. The primary reverse-SSH `localhost:22022` Windows lane rebuilt the dedicated-family ownership slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 1648.33 seconds`; the later projection-deferral hardening rebuild completed in `400.77 seconds`, and the exported automation report passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests with `totalDuration` `41.88494873046875`, `LaunchExecution` duration `41.53509521484375`, and the controller quiet interval reduced to `41.52 seconds`. The current recovery-proof continuation now also deterministically reauthors the dedicated-family maps through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, records manifest-backed owned hashes in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json`, preserves the optional `AHyperTwistHigherDimensionalTrainingShellActor` for manual/editor authoring while intentionally skipping it on the headless commandlet lane because that spawn path still crashes `UnrealEditor-Cmd`, and passes all `13` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests through pulled proof in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json`, including `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`. |
| Higher-dimensional dedicated-family interactive scene ownership | Implemented now | landed `Phase 6C` interactive-scene and dedicated-host continuation packet | First-party current code now promotes the higher-dimensional runtime-session seams into explicit `Magic120Cell` and `MagicCube5D` interactive-scene surfaces with family-owned runtime-state, projection, and persistence envelopes, replaces the shared-map bridge with dedicated-family training-map assets plus dedicated host/launch/view-context/session surfaces, exposes explicit scene-envelope decoders through the runtime library, batches default selector application so generated-mode launch execution no longer rebuilds repository views once per selector, and then defers repository-view projection across the internal generated-mode handoff itself so only the final launch state forces a view refresh. The primary reverse-SSH `localhost:22022` Windows lane rebuilt the dedicated-family ownership slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 1648.33 seconds`; the later projection-deferral hardening rebuild completed in `400.77 seconds`, and the exported automation report passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests with `totalDuration` `41.88494873046875`, `LaunchExecution` duration `41.53509521484375`, and the controller quiet interval reduced to `41.52 seconds`. The current recovery-proof continuation now also deterministically reauthors the dedicated-family maps through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, records manifest-backed owned hashes in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json`, preserves the optional `AHyperTwistHigherDimensionalTrainingShellActor` for manual/editor authoring while intentionally skipping it on the headless commandlet lane because that spawn path still crashes `UnrealEditor-Cmd`, passes all `13` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests through pulled proof in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json`, including `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`, and now has a maintained packaged-validation seam through `scripts/Invoke-HyperTwistHigherDimensionalPackage.ps1` plus `scripts/Launch-HyperTwistHigherDimensionalPackage.ps1`. That helper was then live-validated on `2026-06-22` against isolated worktree `C:\HyperTwist_worktrees\phase10validate` in `-SkipBuild` mode, with `BuildCookRun time: 118.53 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`, both dedicated-family packaged smoke maps green, and pulled aggregate proof in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`. |
| Broad non-Euclidean interaction shell and host ownership | Deep-source grounded retained | `MagicTile` retained remainder | Broad interaction-shell, WinForms/OpenTK host ownership, native spherical/hyperbolic renderer ownership beyond the landed first-party native behavior proof, and generic runtime replacement remain deferred after the landed `Phase 6R-T`, `Phase 7A`, `Phase 7B`, and `Phase 7C` slices, with later native renderer reopening now governed by the separate spec-only `Phase 7D` decision gate. |
### 7. Speech input and voice sidecars
### 8. Speech input and voice sidecars
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
@ -303,7 +313,7 @@ repo.
| Broad voice output / narration sidecars | Deep-source grounded retained | `piper`, `coqui-ai/TTS` retained remainder | Keep actual voice-asset shipping, playback/runtime ownership, and broader assistant-platform scope separate from the landed local review and narration packets. |
| Actual downloadable model and payload shipping | Deep-source grounded retained | `whisper.cpp` retained remainder | Keep real download/shipping ownership separate from the landed bounded custody boundary and from the code-license judgment. |
### 8. Provider-neutral AI/provider routing
### 9. Provider-neutral AI/provider routing
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
@ -319,7 +329,7 @@ repo.
| First-party provider-neutral custom-endpoint runtime routing | Implemented now | landed first-party `Phase 6R-AI` packet | Current bounded per-session provider-profile endpoint selection, runtime routing, fallback-health reflection, and provider-backed transport-failure posture above the landed provider-profile/BYOK and provider-routing seams. OpenAI-compatible custom endpoints are no longer custody-only doctrine targets. |
| Provider-specific overlays | Shallow placeholder | future provider-family work only | Keep internal until source-grounded and normalized. |
### 9. Memory, continuity, notes, and provenance
### 10. Memory, continuity, notes, and provenance
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
@ -337,7 +347,7 @@ repo.
| User-authored note lane | Deep-source grounded retained | memory doctrine, owner unresolved | Explicit note-lane posture/gating is live through `Phase 6R-M5`, but active user-authored note capture still must not be described as shipped until ownership is justified. |
| First-party derived memory and optional adjunct | Implemented now | landed first-party `Phase 6R-M6` packet | Current bounded compact summaries, reduced context packets, and optional digest views are live in explicit derived-only removable form. `Economic-Retention Mode` proves all-features-off correctness by keeping the lane empty and valid, while `Max-Retention Mode` exposes the optional derived adjunct surfaces without promoting them to authoritative truth. |
### 10. Skills and optional assistive layer
### 11. Skills and optional assistive layer
| Feature | Status | Primary authority | Notes |
|---|---|---|---|

View file

@ -78,6 +78,21 @@ Current consolidated milestone snapshot:
- the current packaged validation bridge for that lane is source-controlled via
`C:\HyperTwist\scripts\Invoke-HyperTwistClassicCubePackage.ps1` and
`C:\HyperTwist\scripts\Launch-HyperTwistClassicCubePackage.ps1`
- the higher-dimensional dedicated-family lane now also has a maintained
packaged validation bridge via
`C:\HyperTwist\scripts\Invoke-HyperTwistHigherDimensionalPackage.ps1` and
`C:\HyperTwist\scripts\Launch-HyperTwistHigherDimensionalPackage.ps1`,
which validate the manifest-backed `Magic120Cell` / `MagicCube5D`
dedicated-family map hashes before `BuildCookRun` and emit aggregate plus
per-map smoke JSON under the packaged archive `validation` tree
- that higher-dimensional package lane was then live-validated on `2026-06-22`
through the primary reverse-SSH `localhost:22022` lane against isolated
Windows worktree `C:\HyperTwist_worktrees\phase10validate`, using
`-SkipBuild` on the already-present game receipt, archive output in
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`,
`BuildCookRun time: 118.53 s`, both dedicated-family packaged smoke maps
green, and pulled aggregate proof in
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
- the earlier dedicated-map package proof used the verified fallback reverse-SSH
lane on `localhost:22023`, an isolated Windows worktree at
`C:\HyperTwist_worktrees\phase3to5`, authored dedicated classic/follow-along
@ -171,6 +186,37 @@ Current consolidated milestone snapshot:
including `CoachDashboard.RuntimeInspectSurface`,
`TrainingPanel.RuntimeInspectSurface`, and
`Widget.OperatorStatusSurfaceLive`
- the public HyperTwist website/distribution lane is now also live through a
first-party `website/` app on `2026-06-22`, with rebranded
home/about/resources/pricing/download/legal pages, a protected browser
operator dashboard, shared SuperTokens auth posture, a browser-to-desktop
token handoff seam, a Paddle-ready pricing/check-out structure, and a public
`Open Source Notices` page linked from footer/pricing/download surfaces so
the current MPL distribution doctrine is not left undocumented on public
release surfaces; this lane remains honest that the embedded browser shell is
shipping while the optional full-browser simulator path stays spec-only
- that same website lane also now has a first-party `website/server`
auth/billing backend with `/api/auth/health`, `/api/auth/me`,
`/api/auth/logout`, `/api/auth/desktop-link`,
`/api/auth/desktop-link/verify`, and `/api/billing/paddle/webhook`, while
the browser-to-desktop token lane now also enforces exact website-origin
matching, bounded per-user issuance, and one-time verification-token
consumption with green `website/server` type-check plus focused tests on
`2026-06-22`, and the billing webhook now verifies `Paddle-Signature`
against `PADDLE_WEBHOOK_SECRET` through the documented raw-body HMAC flow,
and the bounded auth-health probe now truthfully distinguishes configured
versus reachable or ready shared SuperTokens core posture while reflecting
fallback-active reason back into the protected dashboard rather than
hardcoding readiness,
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
dashboard, and the browser-to-desktop verify handshake; the public
`/download` page now routes authenticated users
into that protected release surface instead of exposing raw download URLs on
the marketing page itself; production checkout URLs, broader operator/admin
billing workflows, and the exact public corresponding-source URL remain
deployment/application configuration rather than hardcoded product truth
- classic-cube `Phase 9A` replay recording is now closed through first-party
runtime capture, `.json` replay persistence, local playback reconstruction,
schema-light replay normalization across save/load/viewer import, and live
@ -327,6 +373,22 @@ Current consolidated milestone snapshot:
the same `localhost:22022` lane passed all `13`
`HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including
`DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`
- the same `Phase 6C` lane now also has a maintained higher-dimensional
package-validation helper pair,
`scripts/Invoke-HyperTwistHigherDimensionalPackage.ps1` and
`scripts/Launch-HyperTwistHigherDimensionalPackage.ps1`, which insist on
manifest-backed dedicated-family map freshness before cooking
`L_HyperTwist_Magic120CellTraining` and
`L_HyperTwist_MagicCube5DTraining` and then emit aggregate plus per-map
smoke JSON under the packaged archive `validation` tree
- that same higher-dimensional package lane was then live-validated on
`2026-06-22` through the primary reverse-SSH `localhost:22022` lane against
isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate`, using
`-SkipBuild` on the already-present game receipt, archive output in
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim`,
`BuildCookRun time: 118.53 s`, both dedicated-family packaged smoke maps
green, and pulled aggregate proof in
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
- the functional `Phase 6C` ownership gap is now closed; the remaining work
in this lane is optional further micro-profiling and richer non-headless
decorative/manual-authored family map dressing, not missing activation,

View file

@ -0,0 +1,359 @@
param(
[string]$ProjectRoot = 'C:\HyperTwist',
[string]$ArchiveDirectory = 'C:\HyperTwist\packaged\higher-dimensional',
[ValidateSet('Development', 'Shipping')]
[string]$Configuration = 'Development',
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining'),
[string[]]$SmokeMaps = @(),
[string[]]$AdditionalCookerOptions = @(
'-DisablePlugins=MovieRenderPipeline',
'-SkipCookingEditorContent'
),
[string]$AuthoringManifestPath = '',
[string]$ValidationReportPath = '',
[switch]$CleanArchive,
[switch]$SkipBuild,
[switch]$SkipLaunch
)
$ErrorActionPreference = 'Stop'
$RunUatPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\RunUAT.bat'
$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject'
$LaunchScriptPath = Join-Path $ProjectRoot 'scripts\Launch-HyperTwistHigherDimensionalPackage.ps1'
$GameTargetReceiptPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.target'
$UnrealBuildToolSavedPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\UnrealBuildTool'
$CookMaps = @($CookMap) + $AdditionalCookMaps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedSmokeMaps = @(
if ($SmokeMaps.Count -gt 0)
{
$SmokeMaps
}
else
{
$CookMaps
}
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
function Write-Utf8JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[object]$Value
)
$ParentPath = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
{
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
}
$Json = $Value | ConvertTo-Json -Depth 10
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}
function Convert-PathToken {
param(
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value))
{
return 'unknown'
}
return ($Value -replace '[\\/:*?"<>| ]', '_')
}
function Read-JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
function Resolve-PackagedExecutablePath {
param(
[string]$PackageRoot
)
$CandidateExecutablePaths = @(
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'UnrealHyperTwist.exe')
)
return $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
}
function Convert-GameMapPathToContentPath {
param(
[string]$RootPath,
[string]$GameMapPath
)
if (-not $GameMapPath.StartsWith('/Game/'))
{
throw "Cook map '$GameMapPath' is not a supported /Game asset path."
}
$RelativeMapPath = $GameMapPath.Substring('/Game/'.Length).Replace('/', '\')
return Join-Path $RootPath ("UnrealHyperTwist\Content\{0}.umap" -f $RelativeMapPath)
}
function Assert-CookMapExists {
param(
[string]$RootPath,
[string]$GameMapPath
)
$ExpectedMapPath = Convert-GameMapPathToContentPath -RootPath $RootPath -GameMapPath $GameMapPath
if (-not (Test-Path $ExpectedMapPath))
{
throw "Expected higher-dimensional cook map '$GameMapPath' was not found at '$ExpectedMapPath'. Run 'scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1' first."
}
}
if ([string]::IsNullOrWhiteSpace($AuthoringManifestPath))
{
$AuthoringManifestPath = Join-Path $ProjectRoot 'docs\generated\higher_dimensional_training_maps\phase6c_dedicated_family_map_manifest.json'
}
if (-not (Test-Path $RunUatPath))
{
throw "RunUAT was not found at '$RunUatPath'."
}
if (-not (Test-Path $UProjectPath))
{
throw "UnrealHyperTwist project file was not found at '$UProjectPath'."
}
if (-not (Test-Path $AuthoringManifestPath))
{
throw "Higher-dimensional authoring manifest was not found at '$AuthoringManifestPath'. Run 'scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1' first."
}
$AuthoringManifest = Read-JsonFile -Path $AuthoringManifestPath
if ($null -eq $AuthoringManifest)
{
throw "Higher-dimensional authoring manifest '$AuthoringManifestPath' could not be parsed."
}
if ($null -eq $AuthoringManifest.entries -or $AuthoringManifest.entries.Count -lt 2)
{
throw "Higher-dimensional authoring manifest '$AuthoringManifestPath' does not contain the expected dedicated-family entries."
}
$ManifestEntriesByMapPath = @{}
foreach ($ManifestEntry in $AuthoringManifest.entries)
{
if ([string]::IsNullOrWhiteSpace($ManifestEntry.mapAssetPath))
{
throw "Higher-dimensional authoring manifest '$AuthoringManifestPath' contains an entry without mapAssetPath."
}
$ManifestEntriesByMapPath[$ManifestEntry.mapAssetPath] = $ManifestEntry
}
$ValidatedManifestEntries = @()
foreach ($TargetMap in ($CookMaps + $ResolvedSmokeMaps | Select-Object -Unique))
{
Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetMap
if (-not $ManifestEntriesByMapPath.ContainsKey($TargetMap))
{
throw "Higher-dimensional cook map '$TargetMap' is not present in '$AuthoringManifestPath'. Keep this helper scoped to the manifest-backed dedicated-family maps."
}
$ManifestEntry = $ManifestEntriesByMapPath[$TargetMap]
$ExpectedMapPath = Convert-GameMapPathToContentPath -RootPath $ProjectRoot -GameMapPath $TargetMap
$ManifestRelativeMapPath = Join-Path $ProjectRoot ($ManifestEntry.mapFileRelativePath -replace '/', '\')
if ($ExpectedMapPath -ne $ManifestRelativeMapPath)
{
throw "Higher-dimensional manifest entry '$TargetMap' points at '$ManifestRelativeMapPath', but the expected content path resolved to '$ExpectedMapPath'. Re-run dedicated-family map authoring before packaging."
}
if (-not (Test-Path $ManifestRelativeMapPath))
{
throw "Higher-dimensional manifest-backed map file '$ManifestRelativeMapPath' was not found. Run 'scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1' first."
}
$CurrentHash = (Get-FileHash $ManifestRelativeMapPath -Algorithm MD5).Hash.ToLowerInvariant()
$ExpectedHash = [string]$ManifestEntry.mapHashMd5
if ([string]::IsNullOrWhiteSpace($ExpectedHash))
{
throw "Higher-dimensional manifest entry '$TargetMap' does not record mapHashMd5."
}
if ($CurrentHash -ne $ExpectedHash.ToLowerInvariant())
{
throw "Higher-dimensional map '$TargetMap' hash drifted from manifest '$AuthoringManifestPath'. Expected '$ExpectedHash', found '$CurrentHash'. Re-run 'scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1' before packaging."
}
$ValidatedManifestEntries += [ordered]@{
mapKind = [string]$ManifestEntry.mapKind
familyKey = [string]$ManifestEntry.familyKey
mapAssetPath = [string]$ManifestEntry.mapAssetPath
mapFileRelativePath = [string]$ManifestEntry.mapFileRelativePath
mapHashMd5 = $CurrentHash
activationProfileId = [string]$ManifestEntry.activationProfileId
runtimeModeId = [string]$ManifestEntry.runtimeModeId
projectionProfileId = [string]$ManifestEntry.projectionProfileId
primaryPersistenceBoundaryId = [string]$ManifestEntry.primaryPersistenceBoundaryId
}
}
if ($SkipBuild -and -not (Test-Path $GameTargetReceiptPath))
{
throw "SkipBuild was requested, but the packaged-game target receipt was not found at '$GameTargetReceiptPath'. Re-run without -SkipBuild so RunUAT can build the Win64 game target."
}
if ($CleanArchive -and (Test-Path $ArchiveDirectory))
{
Remove-Item -LiteralPath $ArchiveDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $UnrealBuildToolSavedPath | Out-Null
New-Item -ItemType Directory -Force -Path $ArchiveDirectory | Out-Null
New-Item -ItemType Directory -Force -Path $ValidationRootPath | Out-Null
New-Item -ItemType Directory -Force -Path $SmokeReportDirectory | Out-Null
if ([string]::IsNullOrWhiteSpace($ValidationReportPath))
{
$ValidationReportPath = Join-Path $ValidationRootPath 'higher-dimensional-package-validation-report.json'
}
$RunUatArguments = @(
'BuildCookRun',
"-project=$UProjectPath",
'-noP4',
'-platform=Win64',
"-clientconfig=$Configuration",
'-cook',
'-stage',
'-package',
'-pak',
'-archive',
"-archivedirectory=$ArchiveDirectory",
"-map=$($CookMaps -join '+')",
'-unattended',
'-utf8output'
)
if (-not $SkipBuild)
{
$RunUatArguments += '-build'
}
if ($ResolvedAdditionalCookerOptions.Count -gt 0)
{
# Keep the dedicated-family runtime lane insulated from editor-only plugin/content bleed.
$RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}
$ValidationReport = [ordered]@{
reportVersion = 'ht-higher-dimensional-package-validation/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
projectRoot = $ProjectRoot
archiveDirectory = $ArchiveDirectory
configuration = $Configuration
cookMaps = @($CookMaps)
smokeMaps = @($ResolvedSmokeMaps)
additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
cleanArchive = [bool]$CleanArchive
skipBuild = [bool]$SkipBuild
skipLaunch = [bool]$SkipLaunch
authoringManifest = [ordered]@{
path = $AuthoringManifestPath
manifestId = [string]$AuthoringManifest.manifestId
manifestVersion = [string]$AuthoringManifest.manifestVersion
authoredThroughGameModeClassPath = [string]$AuthoringManifest.authoredThroughGameModeClassPath
validatedEntries = @($ValidatedManifestEntries)
}
result = 'failed'
packagedExecutablePath = $null
smokeReports = @()
error = $null
}
try
{
Write-Host "Packaging HyperTwist higher-dimensional validation lane to '$ArchiveDirectory'..."
& $RunUatPath @RunUatArguments
if ($LASTEXITCODE -ne 0)
{
throw "RunUAT packaging failed with exit code $LASTEXITCODE."
}
$PackagedExecutablePath = Resolve-PackagedExecutablePath -PackageRoot $ArchiveDirectory
if ($null -eq $PackagedExecutablePath)
{
throw "No packaged UnrealHyperTwist executable was found beneath '$ArchiveDirectory' after packaging."
}
$ValidationReport.packagedExecutablePath = $PackagedExecutablePath
if (-not $SkipLaunch)
{
if (-not (Test-Path $LaunchScriptPath))
{
throw "Launch script was not found at '$LaunchScriptPath'."
}
foreach ($SmokeMap in $ResolvedSmokeMaps)
{
$SmokeReportPath = Join-Path $SmokeReportDirectory (
'{0}.json' -f (Convert-PathToken -Value $SmokeMap)
)
Write-Host "Smoke validating packaged higher-dimensional map '$SmokeMap'..."
& $LaunchScriptPath -PackageRoot $ArchiveDirectory -MapUrl $SmokeMap -ReportPath $SmokeReportPath
if ($LASTEXITCODE -ne 0)
{
throw "Packaged higher-dimensional smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
}
if (-not (Test-Path $SmokeReportPath))
{
throw "Packaged higher-dimensional smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'."
}
$SmokeReport = Read-JsonFile -Path $SmokeReportPath
if ($null -eq $SmokeReport)
{
throw "Packaged higher-dimensional smoke report '$SmokeReportPath' could not be parsed."
}
if ($SmokeReport.result -ne 'passed')
{
throw "Packaged higher-dimensional smoke report '$SmokeReportPath' did not record a passed result."
}
if ($SmokeReport.mapUrl -ne $SmokeMap)
{
throw "Packaged higher-dimensional smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
}
$ValidationReport.smokeReports += @($SmokeReport)
}
}
$ValidationReport.result = 'passed'
}
catch
{
$ValidationReport.error = $_.Exception.Message
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
throw
}
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport

View file

@ -0,0 +1,126 @@
param(
[string]$PackageRoot = 'C:\HyperTwist\packaged\higher-dimensional',
[string]$MapUrl = '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
[int]$SmokeSeconds = 10,
[int]$ResX = 1600,
[int]$ResY = 900,
[string]$ReportPath = '',
[switch]$KeepRunning
)
$ErrorActionPreference = 'Stop'
function Write-Utf8JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[object]$Value
)
$ParentPath = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
{
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
}
$Json = $Value | ConvertTo-Json -Depth 8
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}
$CandidateExecutablePaths = @(
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'UnrealHyperTwist.exe')
)
$ExecutablePath = $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($null -eq $ExecutablePath)
{
throw "No packaged UnrealHyperTwist executable was found beneath '$PackageRoot'."
}
$ArgumentList = @(
$MapUrl,
"-ResX=$ResX",
"-ResY=$ResY",
'-windowed',
'-log'
)
Write-Host "Launching packaged higher-dimensional validation lane from '$ExecutablePath'..."
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$Process = $null
$Report = [ordered]@{
reportVersion = 'ht-higher-dimensional-package-smoke/v1'
generatedAtUtc = $GeneratedAtUtc
packageRoot = $ResolvedPackageRoot
executablePath = $ExecutablePath
mapUrl = $MapUrl
smokeSeconds = $SmokeSeconds
resolution = [ordered]@{
width = $ResX
height = $ResY
}
keepRunning = [bool]$KeepRunning
result = 'failed'
processId = $null
processStopped = $false
exitCode = $null
error = $null
}
try
{
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
Start-Sleep -Seconds $SmokeSeconds
$Process.Refresh()
$Report.processId = $Process.Id
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
throw "Packaged higher-dimensional executable exited early with code $($Process.ExitCode)."
}
$Report.result = 'passed'
}
catch
{
$Report.error = $_.Exception.Message
if ($null -ne $Process)
{
$Process.Refresh()
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
}
elseif (-not $KeepRunning)
{
Stop-Process -Id $Process.Id -Force
$Report.processStopped = $true
}
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
{
Write-Utf8JsonFile -Path $ReportPath -Value $Report
}
throw
}
Write-Host "Packaged higher-dimensional smoke launch succeeded (PID $($Process.Id))."
if (-not $KeepRunning)
{
Stop-Process -Id $Process.Id -Force
$Report.processStopped = $true
Write-Host 'Stopped packaged higher-dimensional smoke process after successful launch validation.'
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
{
Write-Utf8JsonFile -Path $ReportPath -Value $Report
}

20
website/.env.example Normal file
View file

@ -0,0 +1,20 @@
VITE_SUPERTOKENS_API_DOMAIN=http://localhost:3001
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_GITHUB_OAUTH_ENABLED=false
VITE_GOOGLE_OAUTH_ENABLED=false
VITE_ORCID_OAUTH_ENABLED=false
VITE_PUBLIC_DOCS_URL=
VITE_RELEASE_NOTES_URL=
VITE_SUPPORT_EMAIL=hello@hypertwist.app
VITE_WINDOWS_DOWNLOAD_URL=
VITE_MAC_DOWNLOAD_URL=
VITE_LINUX_DOWNLOAD_URL=
VITE_PADDLE_CHECKOUT_URL_OPERATOR=
VITE_PADDLE_CHECKOUT_URL_STUDIO=
VITE_PLAN_PRICE_OPERATOR=Launch pricing via Paddle
VITE_PLAN_PRICE_STUDIO=Contact for launch readiness
VITE_MPL_SOURCE_URL=
VITE_OPEN_SOURCE_REPO_URL=

66
website/README.md Normal file
View file

@ -0,0 +1,66 @@
# HyperTwist Website
First-party `hypertwist.app` surface for HyperTwist:
- public homepage, about, resources, pricing, support, and legal pages
- browser-facing operator/account dashboard
- shared SuperTokens auth posture reused from the FamiliarOS and ScriptoriumAI website lane
- desktop download posture and desktop-link handshake endpoints
- Paddle-ready pricing/check-out wiring
- public open-source notices surface required by HyperTwist's MPL distribution doctrine
## Why this app exists
HyperTwist already ships an embedded browser runtime inside Unreal under
`Content/Browser/`. That runtime is not the same thing as a public website,
checkout surface, or browser account shell.
This `website/` directory is the dedicated public and operator-facing web
surface for:
- marketing and product positioning
- account/authentication
- billing/pricing posture
- browser dashboard access
- desktop distribution
- public legal and open-source notices
## Local development
```bash
cd /home/dev/src/HyperTwist/website
npm install
npm run dev
```
Frontend default URL:
- `http://localhost:4273`
## Validation
```bash
npm run type-check
npm run test
npm run build
```
## Related surfaces
- frontend app: `website/src/`
- auth server: `website/server/`
- embedded Unreal browser runtime: `Content/Browser/`
- HyperTwist feature authority: `docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md`
- HyperTwist roadmap authority: `docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md`
## Deployment note
Before public launch, configure:
- SuperTokens frontend/backend env vars
- production download URLs
- production Paddle checkout URLs
- the public corresponding-source URL for MPL-covered shipped material
Do not launch the public pricing/download pages without a valid open-source
notices and corresponding-source destination.

23
website/index.html Normal file
View file

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="HyperTwist is a native cube and hypercube training environment for recognition, replay, coaching, higher-dimensional runtime ownership, and desktop-first operator workflows."
/>
<meta name="theme-color" content="#0b1020" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
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"
/>
<title>HyperTwist</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2839
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
website/package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "hypertwist-website",
"version": "1.0.0",
"description": "HyperTwist public website, auth, dashboard, pricing, and download surface.",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@tanstack/react-query": "^5.83.0",
"lucide-react": "^0.525.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",
"supertokens-auth-react": "^0.51.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.14",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "^5.4.19",
"vitest": "^2.1.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -0,0 +1,21 @@
PORT=3001
SUPERTOKENS_CORE_URI=http://localhost:3567
API_DOMAIN=http://localhost:3001
WEBSITE_DOMAIN=http://localhost:4273
API_BASE_PATH=/auth
WEBSITE_BASE_PATH=/auth
COOKIE_SECURE=false
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
DEFAULT_PLAN=free
DEFAULT_ROLE=operator
SUPERTOKENS_HEALTH_TIMEOUT_MS=2000
DESKTOP_LINK_RATE_LIMIT_MAX=5
DESKTOP_LINK_RATE_LIMIT_WINDOW_MS=900000
PADDLE_WEBHOOK_SECRET=
PADDLE_WEBHOOK_TOLERANCE_MS=5000
BILLING_STATE_PATH=
PADDLE_PRODUCT_PLAN_MAP=
PADDLE_PRICE_PLAN_MAP=

63
website/server/README.md Normal file
View file

@ -0,0 +1,63 @@
# HyperTwist Auth Server
Self-hosted SuperTokens auth backend for the HyperTwist website.
Responsibilities:
- email/password sign-up and sign-in
- optional GitHub and Google OAuth
- session bootstrap for the browser dashboard
- browser-to-desktop token handoff with exact-origin checks, bounded issuance, and one-time verification tokens
- 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
## Quick start
```bash
cd /home/dev/src/HyperTwist/website/server
npm install
cp .env.example .env
npm run dev
```
The website frontend should point at this backend through:
```bash
VITE_SUPERTOKENS_API_DOMAIN=http://localhost:3001
VITE_SUPERTOKENS_WEBSITE_DOMAIN=http://localhost:4273
VITE_AUTH_API_BASE_URL=http://localhost:3001
```
## Important note
`/api/billing/paddle/webhook` now verifies the `Paddle-Signature` header
against `PADDLE_WEBHOOK_SECRET` using the documented raw-body `ts:h1`
HMAC-SHA256 flow before it accepts the event.
The desktop-link lane is intentionally stricter than a generic session helper:
- request origin must match the configured website origin exactly
- token issuance is rate-limited per user
- verification consumes the token on first successful use
- verification now returns resolved download-entitlement posture alongside identity, plan, and role
The remaining Paddle deployment work is now configuration, not missing server
ownership:
- real production checkout URLs
- live notification-destination secret management
- event-specific async processing after verification
The server now also supports a small first-party entitlement store:
- `BILLING_STATE_PATH` optionally relocates the persisted billing state file
- `PADDLE_PRODUCT_PLAN_MAP` can map Paddle product IDs to `free` / `operator` / `studio` / `enterprise`
- `PADDLE_PRICE_PLAN_MAP` can map Paddle price IDs to the same plan codes
- if neither map is present, the verified webhook lane can still derive plan from `custom_data.plan`
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

3595
website/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
{
"name": "hypertwist-auth-server",
"version": "1.0.0",
"description": "Self-hosted SuperTokens auth backend for the HyperTwist website.",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"type-check": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"supertokens-node": "^21.1.0"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^24.0.14",
"tsx": "^4.19.2",
"typescript": "^5.8.3",
"vitest": "^2.1.8"
}
}

View file

@ -0,0 +1,84 @@
import { describe, expect, it, vi } from 'vitest'
import { buildSuperTokensApiVersionUrl, probeSuperTokensCoreHealth } from '../auth-health'
describe('buildSuperTokensApiVersionUrl', () => {
it('preserves a configured core path prefix and appends the API-version route', () => {
const result = buildSuperTokensApiVersionUrl({
connectionUri: 'http://localhost:3567/core',
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:4273',
})
expect(result).toBe('http://localhost:3567/core/apiversion?apiDomain=http%3A%2F%2Flocalhost%3A3001&websiteDomain=http%3A%2F%2Flocalhost%3A4273')
})
})
describe('probeSuperTokensCoreHealth', () => {
it('reports ready when the core responds with supported versions', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
new Response(JSON.stringify({
versions: ['2.7', '2.21', '5.0'],
}), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
)
await expect(probeSuperTokensCoreHealth({
connectionUri: 'http://localhost:3567',
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:4273',
fetchImpl,
})).resolves.toEqual({
configured: true,
reachable: true,
ready: true,
apiVersion: '5.0',
error: null,
})
})
it('reports invalid payloads as not ready', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
)
await expect(probeSuperTokensCoreHealth({
connectionUri: 'http://localhost:3567',
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:4273',
fetchImpl,
})).resolves.toEqual({
configured: true,
reachable: false,
ready: false,
apiVersion: null,
error: 'invalid_payload',
})
})
it('reports request failures without throwing', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockRejectedValue(new Error('connect ECONNREFUSED'))
await expect(probeSuperTokensCoreHealth({
connectionUri: 'http://localhost:3567',
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:4273',
fetchImpl,
})).resolves.toEqual({
configured: true,
reachable: false,
ready: false,
apiVersion: null,
error: 'unreachable',
})
})
})

View file

@ -0,0 +1,210 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createBillingStateStore } from '../billing-state'
const tempDirectories: string[] = []
afterEach(() => {
while (tempDirectories.length > 0) {
const directory = tempDirectories.pop()
if (directory) {
fs.rmSync(directory, { recursive: true, force: true })
}
}
})
function createTempStatePath() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'hypertwist-billing-state-'))
tempDirectories.push(directory)
return path.join(directory, 'billing-state.json')
}
describe('createBillingStateStore', () => {
it('applies a subscription activation event and resolves download access', () => {
const store = createBillingStateStore({
statePath: createTempStatePath(),
defaultPlan: 'free',
defaultRole: 'operator',
})
const result = store.applyVerifiedPaddleEvent(JSON.stringify({
event_id: 'evt_activated',
event_type: 'subscription.activated',
occurred_at: '2026-06-22T02:00:00.000Z',
data: {
id: 'sub_123',
customer_id: 'ctm_123',
status: 'active',
custom_data: {
email: 'operator@hypertwist.app',
plan: 'studio',
},
},
}))
expect(result).toEqual({
kind: 'applied',
reason: 'processed',
eventId: 'evt_activated',
eventType: 'subscription.activated',
email: 'operator@hypertwist.app',
})
expect(store.getEntitlementByEmail('operator@hypertwist.app')).toMatchObject({
plan: 'studio',
role: 'operator',
canDownload: true,
accessStatus: 'active',
subscriptionId: 'sub_123',
customerId: 'ctm_123',
source: 'paddle',
})
})
it('deduplicates a previously processed event id', () => {
const store = createBillingStateStore({
statePath: createTempStatePath(),
defaultPlan: 'free',
defaultRole: 'operator',
})
const rawEvent = JSON.stringify({
event_id: 'evt_dupe',
event_type: 'subscription.activated',
occurred_at: '2026-06-22T02:00:00.000Z',
data: {
id: 'sub_dupe',
status: 'active',
custom_data: {
email: 'operator@hypertwist.app',
plan: 'operator',
},
},
})
expect(store.applyVerifiedPaddleEvent(rawEvent).kind).toBe('applied')
expect(store.applyVerifiedPaddleEvent(rawEvent)).toEqual({
kind: 'duplicate',
reason: 'duplicate_event',
eventId: 'evt_dupe',
eventType: 'subscription.activated',
})
})
it('ignores a stale event that arrives after a newer one', () => {
const store = createBillingStateStore({
statePath: createTempStatePath(),
defaultPlan: 'free',
defaultRole: 'operator',
})
expect(store.applyVerifiedPaddleEvent(JSON.stringify({
event_id: 'evt_newer',
event_type: 'subscription.activated',
occurred_at: '2026-06-22T03:00:00.000Z',
data: {
id: 'sub_ordering',
customer_id: 'ctm_ordering',
status: 'active',
custom_data: {
email: 'operator@hypertwist.app',
plan: 'operator',
},
},
})).kind).toBe('applied')
const staleResult = store.applyVerifiedPaddleEvent(JSON.stringify({
event_id: 'evt_older',
event_type: 'subscription.canceled',
occurred_at: '2026-06-22T02:00:00.000Z',
data: {
id: 'sub_ordering',
customer_id: 'ctm_ordering',
status: 'canceled',
},
}))
expect(staleResult).toEqual({
kind: 'ignored',
reason: 'stale_event',
eventId: 'evt_older',
eventType: 'subscription.canceled',
email: 'operator@hypertwist.app',
})
expect(store.getEntitlementByEmail('operator@hypertwist.app')?.accessStatus).toBe('active')
})
it('resolves plans from configured price mappings and revokes on cancellation', () => {
const store = createBillingStateStore({
statePath: createTempStatePath(),
defaultPlan: 'free',
defaultRole: 'operator',
pricePlanMap: {
pri_operator: 'operator',
},
})
expect(store.applyVerifiedPaddleEvent(JSON.stringify({
event_id: 'evt_txn',
event_type: 'transaction.completed',
occurred_at: '2026-06-22T02:00:00.000Z',
data: {
id: 'txn_123',
customer_id: 'ctm_456',
custom_data: {
email: 'operator@hypertwist.app',
},
items: [
{
price: {
id: 'pri_operator',
},
},
],
},
})).kind).toBe('applied')
expect(store.getEntitlementByEmail('operator@hypertwist.app')).toMatchObject({
plan: 'operator',
canDownload: true,
transactionId: 'txn_123',
customerId: 'ctm_456',
})
expect(store.applyVerifiedPaddleEvent(JSON.stringify({
event_id: 'evt_cancel',
event_type: 'subscription.canceled',
occurred_at: '2026-06-22T04:00:00.000Z',
data: {
id: 'sub_789',
customer_id: 'ctm_456',
status: 'canceled',
},
})).kind).toBe('applied')
expect(store.getEntitlementByEmail('operator@hypertwist.app')).toMatchObject({
plan: 'operator',
canDownload: false,
accessStatus: 'canceled',
subscriptionId: 'sub_789',
})
})
it('records unsupported payloads as noops without throwing', () => {
const store = createBillingStateStore({
statePath: createTempStatePath(),
defaultPlan: 'free',
defaultRole: 'operator',
})
expect(store.applyVerifiedPaddleEvent('{"not":"a-paddle-event"}')).toEqual({
kind: 'noop',
reason: 'unsupported_payload',
eventId: 'unknown',
eventType: 'unknown',
})
})
})

View file

@ -0,0 +1,97 @@
import crypto from 'node:crypto'
import { describe, expect, it } from 'vitest'
import { verifyPaddleWebhookSignature } from '../paddle-webhook'
function buildSignatureHeader({
rawBody,
secretKey,
timestamp,
}: {
rawBody: string
secretKey: string
timestamp: string
}) {
const signature = crypto
.createHmac('sha256', secretKey)
.update(`${timestamp}:${rawBody}`)
.digest('hex')
return `ts=${timestamp};h1=${signature}`
}
describe('verifyPaddleWebhookSignature', () => {
it('accepts a valid signature built from the raw body', () => {
const rawBody = JSON.stringify({ event_id: 'evt_123', event_type: 'transaction.completed' })
const timestamp = '1719012345'
const secretKey = 'pdl_ntfset_test_secret'
const result = verifyPaddleWebhookSignature({
rawBody,
signatureHeader: buildSignatureHeader({ rawBody, secretKey, timestamp }),
secretKey,
toleranceMs: 5_000,
now: () => Number(timestamp) * 1_000 + 1_000,
})
expect(result).toEqual({ ok: true, timestamp })
})
it('rejects missing or malformed headers', () => {
const rawBody = '{}'
const secretKey = 'pdl_ntfset_test_secret'
expect(verifyPaddleWebhookSignature({
rawBody,
signatureHeader: '',
secretKey,
})).toEqual({ ok: false, error: 'missing_signature' })
expect(verifyPaddleWebhookSignature({
rawBody,
signatureHeader: 'ts=1719012345',
secretKey,
})).toEqual({ ok: false, error: 'invalid_signature_header' })
})
it('rejects signatures that are outside the tolerance window', () => {
const rawBody = '{}'
const timestamp = '1719012345'
const secretKey = 'pdl_ntfset_test_secret'
const result = verifyPaddleWebhookSignature({
rawBody,
signatureHeader: buildSignatureHeader({ rawBody, secretKey, timestamp }),
secretKey,
toleranceMs: 5_000,
now: () => Number(timestamp) * 1_000 + 6_000,
})
expect(result).toEqual({ ok: false, error: 'expired_signature' })
})
it('rejects mismatched signatures', () => {
const rawBody = '{}'
const secretKey = 'pdl_ntfset_test_secret'
const result = verifyPaddleWebhookSignature({
rawBody,
signatureHeader: 'ts=1719012345;h1=deadbeef',
secretKey,
toleranceMs: 5_000,
now: () => 1719012345_000 + 1_000,
})
expect(result).toEqual({ ok: false, error: 'invalid_signature' })
})
it('fails closed when the secret key is missing', () => {
const result = verifyPaddleWebhookSignature({
rawBody: '{}',
signatureHeader: 'ts=1719012345;h1=deadbeef',
secretKey: '',
})
expect(result).toEqual({ ok: false, error: 'missing_secret' })
})
})

View file

@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest'
import { buildAllowedOriginMatcher, createDesktopLinkStore } from '../security'
describe('buildAllowedOriginMatcher', () => {
it('accepts the configured origin and same-origin referrer paths', () => {
const matchesAllowedOrigin = buildAllowedOriginMatcher('https://hypertwist.app')
expect(matchesAllowedOrigin('https://hypertwist.app')).toBe(true)
expect(matchesAllowedOrigin('https://hypertwist.app/app/downloads')).toBe(true)
})
it('rejects prefix-spoofed or unrelated origins', () => {
const matchesAllowedOrigin = buildAllowedOriginMatcher('https://hypertwist.app')
expect(matchesAllowedOrigin('https://hypertwist.app.evil.example')).toBe(false)
expect(matchesAllowedOrigin('https://evil.example/hypertwist.app')).toBe(false)
expect(matchesAllowedOrigin('not-a-url')).toBe(false)
})
})
describe('createDesktopLinkStore', () => {
it('issues one-time desktop-link tokens and consumes them on verify', () => {
let currentTime = 100
let seed = 1
const store = createDesktopLinkStore({
ttlMs: 60_000,
rateLimitMax: 5,
rateLimitWindowMs: 60_000,
now: () => currentTime,
randomBytes: () => Buffer.from((seed++).toString(16).padStart(64, '0'), 'hex'),
})
const issue = store.issueToken({
rateLimitKey: 'desktop-link:user-1',
userId: 'user-1',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: true,
accessStatus: 'active',
})
expect(issue.ok).toBe(true)
if (!issue.ok) {
return
}
const firstVerify = store.verifyAndConsumeToken(issue.token)
expect(firstVerify?.email).toBe('operator@hypertwist.app')
expect(firstVerify?.canDownload).toBe(true)
expect(firstVerify?.accessStatus).toBe('active')
expect(store.verifyAndConsumeToken(issue.token)).toBeNull()
expect(store.getTokenCount()).toBe(0)
})
it('rate limits repeated issuance inside the configured window', () => {
let currentTime = 1_000
let seed = 10
const store = createDesktopLinkStore({
ttlMs: 60_000,
rateLimitMax: 2,
rateLimitWindowMs: 5_000,
now: () => currentTime,
randomBytes: () => Buffer.from((seed++).toString(16).padStart(64, '0'), 'hex'),
})
expect(store.issueToken({
rateLimitKey: 'desktop-link:user-2',
userId: 'user-2',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: true,
accessStatus: 'active',
}).ok).toBe(true)
expect(store.issueToken({
rateLimitKey: 'desktop-link:user-2',
userId: 'user-2',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: true,
accessStatus: 'active',
}).ok).toBe(true)
expect(store.issueToken({
rateLimitKey: 'desktop-link:user-2',
userId: 'user-2',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: true,
accessStatus: 'active',
})).toEqual({ ok: false, error: 'rate_limited' })
currentTime += 5_001
expect(store.issueToken({
rateLimitKey: 'desktop-link:user-2',
userId: 'user-2',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: true,
accessStatus: 'active',
}).ok).toBe(true)
})
it('rejects expired or malformed verification tokens', () => {
let currentTime = 5_000
const store = createDesktopLinkStore({
ttlMs: 500,
rateLimitMax: 5,
rateLimitWindowMs: 60_000,
now: () => currentTime,
randomBytes: () => Buffer.from('ab'.repeat(32), 'hex'),
})
const issue = store.issueToken({
rateLimitKey: 'desktop-link:user-3',
userId: 'user-3',
email: 'operator@hypertwist.app',
plan: 'operator',
role: 'operator',
canDownload: false,
accessStatus: 'paused',
})
expect(store.verifyAndConsumeToken('bad-token')).toBeNull()
expect(issue.ok).toBe(true)
if (!issue.ok) {
return
}
currentTime += 501
expect(store.verifyAndConsumeToken(issue.token)).toBeNull()
expect(store.getTokenCount()).toBe(0)
})
})

View file

@ -0,0 +1,157 @@
interface SuperTokensCoreHealthProbeOptions {
connectionUri: string
apiDomain: string
websiteDomain: string
timeoutMs?: number
fetchImpl?: typeof fetch
}
export interface SuperTokensCoreHealthSnapshot {
configured: boolean
reachable: boolean
ready: boolean
apiVersion: string | null
error: string | null
}
function normalizeTrimmed(value: string) {
return value.trim()
}
function parseApiVersions(payload: unknown): string[] {
if (!payload || typeof payload !== 'object') {
return []
}
const rawVersions = (payload as { versions?: unknown }).versions
if (!Array.isArray(rawVersions)) {
return []
}
return rawVersions
.map((value) => String(value || '').trim())
.filter(Boolean)
}
function compareVersionNumbers(left: string, right: string) {
const leftParts = left.split('.').map((part) => Number(part))
const rightParts = right.split('.').map((part) => Number(part))
const maxLength = Math.max(leftParts.length, rightParts.length)
for (let index = 0; index < maxLength; index += 1) {
const leftValue = Number.isFinite(leftParts[index]) ? leftParts[index] : 0
const rightValue = Number.isFinite(rightParts[index]) ? rightParts[index] : 0
if (leftValue > rightValue) return 1
if (leftValue < rightValue) return -1
}
return 0
}
function pickHighestVersion(versions: string[]) {
if (versions.length === 0) {
return null
}
return versions.reduce((highest, candidate) => (
compareVersionNumbers(candidate, highest) > 0 ? candidate : highest
))
}
export function buildSuperTokensApiVersionUrl({
connectionUri,
apiDomain,
websiteDomain,
}: Pick<SuperTokensCoreHealthProbeOptions, 'connectionUri' | 'apiDomain' | 'websiteDomain'>) {
const url = new URL(normalizeTrimmed(connectionUri))
const nextPath = `${url.pathname.replace(/\/+$/, '')}/apiversion`.replace(/\/{2,}/g, '/')
url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`
url.search = ''
url.searchParams.set('apiDomain', normalizeTrimmed(apiDomain))
url.searchParams.set('websiteDomain', normalizeTrimmed(websiteDomain))
return url.toString()
}
export async function probeSuperTokensCoreHealth({
connectionUri,
apiDomain,
websiteDomain,
timeoutMs = 2_000,
fetchImpl = fetch,
}: SuperTokensCoreHealthProbeOptions): Promise<SuperTokensCoreHealthSnapshot> {
const normalizedConnectionUri = normalizeTrimmed(connectionUri)
const normalizedApiDomain = normalizeTrimmed(apiDomain)
const normalizedWebsiteDomain = normalizeTrimmed(websiteDomain)
const configured = Boolean(normalizedConnectionUri && normalizedApiDomain && normalizedWebsiteDomain)
if (!configured) {
return {
configured: false,
reachable: false,
ready: false,
apiVersion: null,
error: 'not_configured',
}
}
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), Math.max(250, timeoutMs))
timeout.unref?.()
try {
const response = await fetchImpl(
buildSuperTokensApiVersionUrl({
connectionUri: normalizedConnectionUri,
apiDomain: normalizedApiDomain,
websiteDomain: normalizedWebsiteDomain,
}),
{
method: 'GET',
headers: {
accept: 'application/json',
},
signal: controller.signal,
},
)
if (!response.ok) {
return {
configured: true,
reachable: false,
ready: false,
apiVersion: null,
error: `http_${response.status}`,
}
}
const versions = parseApiVersions(await response.json())
const apiVersion = pickHighestVersion(versions)
if (!apiVersion) {
return {
configured: true,
reachable: false,
ready: false,
apiVersion: null,
error: 'invalid_payload',
}
}
return {
configured: true,
reachable: true,
ready: true,
apiVersion,
error: null,
}
} catch (error) {
return {
configured: true,
reachable: false,
ready: false,
apiVersion: null,
error: error instanceof Error && error.name === 'AbortError' ? 'timeout' : 'unreachable',
}
} finally {
clearTimeout(timeout)
}
}

View file

@ -0,0 +1,475 @@
import fs from 'node:fs'
import path from 'node:path'
export type BillingPlan = 'free' | 'operator' | 'studio' | 'enterprise'
export type BillingRole = 'viewer' | 'operator' | 'reviewer' | 'admin'
export interface BillingEntitlementState {
email: string
plan: BillingPlan
role: BillingRole
canDownload: boolean
accessStatus: string
source: 'paddle'
customerId: string | null
subscriptionId: string | null
transactionId: string | null
lastEventId: string
lastEventType: string
lastEventAt: string
updatedAt: string
}
interface BillingStateFile {
version: 'ht-billing-state/v1'
updatedAt: string
processedEvents: Record<string, string>
accounts: Record<string, BillingEntitlementState>
}
interface PaddleEventEnvelope {
event_id: string
event_type: string
occurred_at: string
data: Record<string, unknown>
}
interface BillingStateStoreOptions {
statePath: string
defaultPlan: BillingPlan
defaultRole: BillingRole
productPlanMap?: Record<string, BillingPlan>
pricePlanMap?: Record<string, BillingPlan>
now?: () => Date
}
export interface BillingEventApplyResult {
kind: 'applied' | 'duplicate' | 'ignored' | 'noop'
reason:
| 'processed'
| 'duplicate_event'
| 'stale_event'
| 'unsupported_payload'
| 'unresolved_account'
eventId: string
eventType: string
email?: string
}
function normalizeEmail(value: unknown): string {
return String(value || '').trim().toLowerCase()
}
function normalizePlan(value: unknown, fallback: BillingPlan): BillingPlan {
const plan = String(value || '').trim().toLowerCase()
if (plan === 'operator' || plan === 'studio' || plan === 'enterprise' || plan === 'free') {
return plan
}
if (plan === 'explorer') {
return 'free'
}
return fallback
}
function normalizeRole(value: unknown, fallback: BillingRole): BillingRole {
const role = String(value || '').trim().toLowerCase()
if (role === 'viewer' || role === 'operator' || role === 'reviewer' || role === 'admin') {
return role
}
return fallback
}
function ensureParentDirectory(targetPath: string) {
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
}
function readJsonFile(targetPath: string): BillingStateFile | null {
if (!fs.existsSync(targetPath)) {
return null
}
const raw = fs.readFileSync(targetPath, 'utf8')
return JSON.parse(raw) as BillingStateFile
}
function createEmptyState(nowIso: string): BillingStateFile {
return {
version: 'ht-billing-state/v1',
updatedAt: nowIso,
processedEvents: {},
accounts: {},
}
}
function writeJsonFile(targetPath: string, data: BillingStateFile) {
ensureParentDirectory(targetPath)
const tempPath = `${targetPath}.tmp`
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8')
fs.renameSync(tempPath, targetPath)
}
function toIsoString(now: Date): string {
return now.toISOString()
}
function isLaterEvent(existingOccurredAt: string, nextOccurredAt: string): boolean {
return Date.parse(nextOccurredAt) > Date.parse(existingOccurredAt)
}
function readStringAtPath(root: unknown, pathSegments: Array<string | number>): string | null {
let current: unknown = root
for (const segment of pathSegments) {
if (current == null) return null
if (typeof segment === 'number') {
if (!Array.isArray(current) || current.length <= segment) return null
current = current[segment]
continue
}
if (typeof current !== 'object' || !(segment in current)) return null
current = (current as Record<string, unknown>)[segment]
}
const value = String(current || '').trim()
return value ? value : null
}
function readRecordAtPath(root: unknown, pathSegments: Array<string | number>): Record<string, unknown> | null {
let current: unknown = root
for (const segment of pathSegments) {
if (current == null) return null
if (typeof segment === 'number') {
if (!Array.isArray(current) || current.length <= segment) return null
current = current[segment]
continue
}
if (typeof current !== 'object' || !(segment in current)) return null
current = (current as Record<string, unknown>)[segment]
}
return current && typeof current === 'object' && !Array.isArray(current)
? current as Record<string, unknown>
: null
}
function readEmailFromEventData(data: Record<string, unknown>): string {
const candidates = [
['customer', 'email'],
['customer', 'email_address'],
['custom_data', 'email'],
['custom_data', 'user_email'],
['custom_data', 'account_email'],
['custom_data', 'operator_email'],
['custom_data', 'hypertwist_account_email'],
['billing_details', 'email'],
]
for (const candidate of candidates) {
const value = readStringAtPath(data, candidate)
const email = normalizeEmail(value)
if (email) {
return email
}
}
return ''
}
function readAllCandidateIds(data: Record<string, unknown>, key: 'subscriptionId' | 'transactionId' | 'customerId') {
const pathCandidates: Record<typeof key, Array<Array<string | number>>> = {
subscriptionId: [
['id'],
['subscription_id'],
['data', 'id'],
],
transactionId: [
['transaction_id'],
['id'],
['data', 'transaction_id'],
],
customerId: [
['customer_id'],
['customer', 'id'],
['data', 'customer_id'],
],
}
const values = new Set<string>()
for (const candidate of pathCandidates[key]) {
const value = readStringAtPath(data, candidate)
if (value) {
values.add(value)
}
}
return Array.from(values)
}
function readPlanFromCustomData(data: Record<string, unknown>, fallback: BillingPlan): BillingPlan {
const customData = readRecordAtPath(data, ['custom_data'])
if (!customData) {
return fallback
}
return normalizePlan(
customData.plan
?? customData.ht_plan
?? customData.hypertwist_plan
?? customData.account_plan,
fallback,
)
}
function readRoleFromCustomData(data: Record<string, unknown>, fallback: BillingRole): BillingRole {
const customData = readRecordAtPath(data, ['custom_data'])
if (!customData) {
return fallback
}
return normalizeRole(
customData.role
?? customData.ht_role
?? customData.hypertwist_role
?? customData.account_role,
fallback,
)
}
function resolvePlanFromItemMaps(
data: Record<string, unknown>,
productPlanMap: Record<string, BillingPlan>,
pricePlanMap: Record<string, BillingPlan>,
): BillingPlan | null {
const items = Array.isArray(data.items) ? data.items : []
for (const item of items) {
if (!item || typeof item !== 'object') continue
const record = item as Record<string, unknown>
const priceId = readStringAtPath(record, ['price', 'id']) ?? readStringAtPath(record, ['price_id'])
if (priceId && pricePlanMap[priceId]) {
return pricePlanMap[priceId]
}
const productId =
readStringAtPath(record, ['product', 'id'])
?? readStringAtPath(record, ['product_id'])
?? readStringAtPath(record, ['price', 'product_id'])
if (productId && productPlanMap[productId]) {
return productPlanMap[productId]
}
}
return null
}
function inferAccessStatus(eventType: string, data: Record<string, unknown>): string {
const explicitStatus = readStringAtPath(data, ['status'])
if (explicitStatus) {
return explicitStatus
}
if (eventType === 'subscription.trialing') return 'trialing'
if (eventType === 'subscription.activated' || eventType === 'subscription.resumed') return 'active'
if (eventType === 'subscription.paused') return 'paused'
if (eventType === 'subscription.canceled') return 'canceled'
if (eventType === 'subscription.past_due' || eventType === 'transaction.past_due' || eventType === 'transaction.payment_failed') return 'past_due'
if (eventType === 'transaction.completed' || eventType === 'transaction.paid') return 'completed'
if (eventType === 'transaction.canceled') return 'canceled'
return 'unknown'
}
function deriveCanDownload(plan: BillingPlan, accessStatus: string): boolean {
if (plan === 'free') {
return false
}
return accessStatus === 'active'
|| accessStatus === 'trialing'
|| accessStatus === 'completed'
|| accessStatus === 'paid'
}
function parsePaddleEvent(rawBody: string): PaddleEventEnvelope | null {
try {
const parsed = JSON.parse(rawBody) as Partial<PaddleEventEnvelope>
if (
!parsed
|| typeof parsed !== 'object'
|| typeof parsed.event_id !== 'string'
|| typeof parsed.event_type !== 'string'
|| typeof parsed.occurred_at !== 'string'
|| !parsed.data
|| typeof parsed.data !== 'object'
|| Array.isArray(parsed.data)
) {
return null
}
return parsed as PaddleEventEnvelope
} catch {
return null
}
}
export function createBillingStateStore({
statePath,
defaultPlan,
defaultRole,
productPlanMap = {},
pricePlanMap = {},
now = () => new Date(),
}: BillingStateStoreOptions) {
function loadState(): BillingStateFile {
const loaded = readJsonFile(statePath)
if (!loaded || loaded.version !== 'ht-billing-state/v1') {
return createEmptyState(toIsoString(now()))
}
return loaded
}
function saveState(state: BillingStateFile) {
state.updatedAt = toIsoString(now())
writeJsonFile(statePath, state)
}
function findExistingAccount(
state: BillingStateFile,
email: string,
ids: {
subscriptionIds: string[]
transactionIds: string[]
customerIds: string[]
},
): BillingEntitlementState | null {
if (email && state.accounts[email]) {
return state.accounts[email]
}
for (const account of Object.values(state.accounts)) {
if (
(account.subscriptionId && ids.subscriptionIds.includes(account.subscriptionId))
|| (account.transactionId && ids.transactionIds.includes(account.transactionId))
|| (account.customerId && ids.customerIds.includes(account.customerId))
) {
return account
}
}
return null
}
function getEntitlementByEmail(email: string): BillingEntitlementState | null {
const normalizedEmail = normalizeEmail(email)
if (!normalizedEmail) {
return null
}
const state = loadState()
return state.accounts[normalizedEmail] || null
}
function applyVerifiedPaddleEvent(rawBody: string): BillingEventApplyResult {
const envelope = parsePaddleEvent(rawBody)
if (!envelope) {
return {
kind: 'noop',
reason: 'unsupported_payload',
eventId: 'unknown',
eventType: 'unknown',
}
}
const state = loadState()
if (state.processedEvents[envelope.event_id]) {
return {
kind: 'duplicate',
reason: 'duplicate_event',
eventId: envelope.event_id,
eventType: envelope.event_type,
}
}
const email = readEmailFromEventData(envelope.data)
const ids = {
subscriptionIds: [
readStringAtPath(envelope.data, ['id']) && envelope.event_type.startsWith('subscription.')
? String(readStringAtPath(envelope.data, ['id']))
: '',
...readAllCandidateIds(envelope.data, 'subscriptionId'),
].filter(Boolean),
transactionIds: [
readStringAtPath(envelope.data, ['id']) && envelope.event_type.startsWith('transaction.')
? String(readStringAtPath(envelope.data, ['id']))
: '',
...readAllCandidateIds(envelope.data, 'transactionId'),
].filter(Boolean),
customerIds: readAllCandidateIds(envelope.data, 'customerId'),
}
const existingAccount = findExistingAccount(state, email, ids)
if (existingAccount && !isLaterEvent(existingAccount.lastEventAt, envelope.occurred_at)) {
state.processedEvents[envelope.event_id] = envelope.occurred_at
saveState(state)
return {
kind: 'ignored',
reason: 'stale_event',
eventId: envelope.event_id,
eventType: envelope.event_type,
email: existingAccount.email,
}
}
const resolvedEmail = email || existingAccount?.email || ''
if (!resolvedEmail) {
state.processedEvents[envelope.event_id] = envelope.occurred_at
saveState(state)
return {
kind: 'noop',
reason: 'unresolved_account',
eventId: envelope.event_id,
eventType: envelope.event_type,
}
}
const planFromItems = resolvePlanFromItemMaps(envelope.data, productPlanMap, pricePlanMap)
const nextPlan = planFromItems
?? readPlanFromCustomData(envelope.data, existingAccount?.plan ?? defaultPlan)
const nextRole = readRoleFromCustomData(envelope.data, existingAccount?.role ?? defaultRole)
const accessStatus = inferAccessStatus(envelope.event_type, envelope.data)
const nextState: BillingEntitlementState = {
email: resolvedEmail,
plan: nextPlan,
role: nextRole,
canDownload: deriveCanDownload(nextPlan, accessStatus),
accessStatus,
source: 'paddle',
customerId: ids.customerIds[0] || existingAccount?.customerId || null,
subscriptionId: ids.subscriptionIds[0] || existingAccount?.subscriptionId || null,
transactionId: ids.transactionIds[0] || existingAccount?.transactionId || null,
lastEventId: envelope.event_id,
lastEventType: envelope.event_type,
lastEventAt: envelope.occurred_at,
updatedAt: toIsoString(now()),
}
state.accounts[resolvedEmail] = nextState
state.processedEvents[envelope.event_id] = envelope.occurred_at
saveState(state)
return {
kind: 'applied',
reason: 'processed',
eventId: envelope.event_id,
eventType: envelope.event_type,
email: resolvedEmail,
}
}
function getProcessedEventCount() {
return Object.keys(loadState().processedEvents).length
}
return {
applyVerifiedPaddleEvent,
getEntitlementByEmail,
getProcessedEventCount,
getStatePath: () => statePath,
}
}

409
website/server/src/index.ts Normal file
View file

@ -0,0 +1,409 @@
import 'dotenv/config'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import path from 'node:path'
import supertokens from 'supertokens-node'
import { errorHandler, middleware, type SessionRequest } from 'supertokens-node/framework/express'
import EmailPassword from 'supertokens-node/recipe/emailpassword'
import Session from 'supertokens-node/recipe/session'
import ThirdParty from 'supertokens-node/recipe/thirdparty'
import GithubProvider from 'supertokens-node/lib/build/recipe/thirdparty/providers/github'
import GoogleProvider from 'supertokens-node/lib/build/recipe/thirdparty/providers/google'
import { probeSuperTokensCoreHealth } from './auth-health'
import { createBillingStateStore, type BillingPlan, type BillingRole } from './billing-state'
import { verifyPaddleWebhookSignature } from './paddle-webhook'
import { buildAllowedOriginMatcher, createDesktopLinkStore } from './security'
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>
const PORT = Number(process.env.PORT || 3001)
const SUPERTOKENS_CORE_URI = process.env.SUPERTOKENS_CORE_URI || 'http://localhost:3567'
const API_DOMAIN = process.env.API_DOMAIN || `http://localhost:${PORT}`
const WEBSITE_DOMAIN = process.env.WEBSITE_DOMAIN || 'http://localhost:4273'
const API_BASE_PATH = process.env.API_BASE_PATH || '/auth'
const WEBSITE_BASE_PATH = process.env.WEBSITE_BASE_PATH || '/auth'
const COOKIE_SECURE = String(process.env.COOKIE_SECURE || '').toLowerCase() === 'true'
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 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)
const DESKTOP_LINK_TTL_MS = 10 * 60 * 1000
const DESKTOP_LINK_RATE_LIMIT_MAX = Number(process.env.DESKTOP_LINK_RATE_LIMIT_MAX || 5)
const DESKTOP_LINK_RATE_LIMIT_WINDOW_MS = Number(process.env.DESKTOP_LINK_RATE_LIMIT_WINDOW_MS || 15 * 60 * 1000)
const PADDLE_WEBHOOK_SECRET = process.env.PADDLE_WEBHOOK_SECRET || ''
const PADDLE_WEBHOOK_TOLERANCE_MS = Number(process.env.PADDLE_WEBHOOK_TOLERANCE_MS || 5_000)
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)
function normalizeBillingPlan(value: string): BillingPlan {
const plan = value.trim().toLowerCase()
if (plan === 'operator' || plan === 'studio' || plan === 'enterprise' || plan === 'free') {
return plan
}
if (plan === 'explorer') {
return 'free'
}
return 'free'
}
function normalizeBillingRole(value: string): BillingRole {
const role = value.trim().toLowerCase()
if (role === 'viewer' || role === 'operator' || role === 'reviewer' || role === 'admin') {
return role
}
return 'operator'
}
function parseBillingPlanMap(rawValue: string | undefined): Record<string, BillingPlan> {
const trimmed = String(rawValue || '').trim()
if (!trimmed) {
return {}
}
try {
const parsed = JSON.parse(trimmed) as Record<string, string>
return Object.fromEntries(
Object.entries(parsed)
.filter(([key]) => String(key || '').trim().length > 0)
.map(([key, value]) => [key, normalizeBillingPlan(String(value || ''))]),
)
} catch {
console.warn('[billing] failed to parse plan map JSON; ignoring configured map')
return {}
}
}
function resolveAccountState(email: string, payload: Record<string, unknown>) {
const entitlement = billingStateStore.getEntitlementByEmail(email)
const plan = entitlement?.plan ?? normalizeBillingPlan(String(payload.plan || DEFAULT_PLAN))
const role = entitlement?.role ?? normalizeBillingRole(String(payload.role || DEFAULT_ROLE))
const canDownload = entitlement ? entitlement.canDownload : plan !== 'free'
return {
plan,
role,
canDownload,
billing: {
source: entitlement?.source || 'session',
access_status: entitlement?.accessStatus || 'session-default',
can_download: canDownload,
subscription_id: entitlement?.subscriptionId || null,
customer_id: entitlement?.customerId || null,
transaction_id: entitlement?.transactionId || null,
last_event_id: entitlement?.lastEventId || null,
last_event_type: entitlement?.lastEventType || null,
last_event_at: entitlement?.lastEventAt || null,
updated_at: entitlement?.updatedAt || null,
state_path: entitlement ? billingStateStore.getStatePath() : null,
},
}
}
const recipeList = [
EmailPassword.init({
signUpFeature: {
formFields: [
{ id: 'email' },
{ id: 'password' },
],
},
override: {
apis: (originalImplementation) => ({
...originalImplementation,
signUpPOST: async (input) => {
const response = await originalImplementation.signUpPOST!(input)
if (response.status === 'OK') {
await response.session.mergeIntoAccessTokenPayload({
email: response.user.emails[0],
plan: DEFAULT_PLAN,
role: DEFAULT_ROLE,
})
}
return response
},
}),
},
}),
Session.init({
cookieSecure: COOKIE_SECURE,
cookieSameSite: COOKIE_SECURE ? 'none' : 'lax',
}),
]
const thirdPartyProviders = []
if (GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET) {
thirdPartyProviders.push(Github({
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
scope: ['read:user', 'user:email'],
}))
}
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
thirdPartyProviders.push(Google({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
scope: ['openid', 'email', 'profile'],
}))
}
if (thirdPartyProviders.length > 0) {
recipeList.splice(1, 0, ThirdParty.init({
signInAndUpFeature: {
providers: thirdPartyProviders,
},
override: {
apis: (originalImplementation) => ({
...originalImplementation,
signInUpPOST: async (input) => {
const response = await originalImplementation.signInUpPOST!(input)
if (response.status === 'OK') {
await response.session.mergeIntoAccessTokenPayload({
email: response.user.emails[0] || '',
plan: DEFAULT_PLAN,
role: DEFAULT_ROLE,
})
}
return response
},
}),
},
}))
}
supertokens.init({
framework: 'express',
supertokens: {
connectionURI: SUPERTOKENS_CORE_URI,
},
appInfo: {
appName: 'HyperTwist',
apiDomain: API_DOMAIN,
websiteDomain: WEBSITE_DOMAIN,
apiBasePath: API_BASE_PATH,
websiteBasePath: WEBSITE_BASE_PATH,
},
recipeList,
})
const matchesAllowedOrigin = buildAllowedOriginMatcher(WEBSITE_DOMAIN)
const desktopLinkStore = createDesktopLinkStore({
ttlMs: DESKTOP_LINK_TTL_MS,
rateLimitMax: DESKTOP_LINK_RATE_LIMIT_MAX,
rateLimitWindowMs: DESKTOP_LINK_RATE_LIMIT_WINDOW_MS,
})
const billingStateStore = createBillingStateStore({
statePath: BILLING_STATE_PATH,
defaultPlan: DEFAULT_PLAN,
defaultRole: DEFAULT_ROLE,
productPlanMap: PADDLE_PRODUCT_PLAN_MAP,
pricePlanMap: PADDLE_PRICE_PLAN_MAP,
})
setInterval(() => {
desktopLinkStore.cleanupExpiredTokens()
}, 60_000).unref()
const app = express()
app.post('/api/billing/paddle/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = Buffer.isBuffer(req.body) ? req.body.toString() : ''
const verificationResult = verifyPaddleWebhookSignature({
rawBody,
signatureHeader: req.headers['paddle-signature'],
secretKey: PADDLE_WEBHOOK_SECRET,
toleranceMs: PADDLE_WEBHOOK_TOLERANCE_MS,
})
if (!verificationResult.ok) {
const statusCode = verificationResult.error === 'missing_secret'
? 503
: verificationResult.error === 'expired_signature'
? 408
: 400
console.warn('[billing] rejected paddle webhook', {
error: verificationResult.error,
bodyBytes: Buffer.byteLength(rawBody),
})
return res.status(statusCode).json({ error: verificationResult.error })
}
const applyResult = billingStateStore.applyVerifiedPaddleEvent(rawBody)
console.log('[billing] verified paddle webhook received', {
bodyBytes: Buffer.byteLength(rawBody),
timestamp: verificationResult.timestamp,
applyResult,
})
return res.json({ received: true, verified: true, apply_result: applyResult })
})
app.use(cors({
origin(origin, callback) {
if (!origin || matchesAllowedOrigin(origin)) {
callback(null, true)
return
}
callback(new Error('forbidden_origin'))
},
credentials: true,
allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
}))
app.use(express.json())
app.use(cookieParser())
app.use(middleware())
app.get('/health', (_req, res) => {
res.json({ ok: true, service: 'hypertwist-auth-server' })
})
app.get('/api/auth/health', async (_req, res) => {
const superTokensHealth = await probeSuperTokensCoreHealth({
connectionUri: SUPERTOKENS_CORE_URI,
apiDomain: API_DOMAIN,
websiteDomain: WEBSITE_DOMAIN,
timeoutMs: SUPERTOKENS_HEALTH_TIMEOUT_MS,
})
res.json({
ok: true,
service: 'hypertwist-auth-server',
supertokens: {
configured: superTokensHealth.configured,
reachable: superTokensHealth.reachable,
ready: superTokensHealth.ready,
apiVersion: superTokensHealth.apiVersion,
error: superTokensHealth.error,
oauth: {
github: Boolean(GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET),
google: Boolean(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET),
},
},
fallback: {
enabled: true,
active: !superTokensHealth.ready,
reason: superTokensHealth.ready ? null : (superTokensHealth.error || 'not_ready'),
},
billing: {
statePath: billingStateStore.getStatePath(),
processedEventCount: billingStateStore.getProcessedEventCount(),
pricePlanMapConfigured: Object.keys(PADDLE_PRICE_PLAN_MAP).length > 0,
productPlanMapConfigured: Object.keys(PADDLE_PRODUCT_PLAN_MAP).length > 0,
webhookSecretConfigured: Boolean(PADDLE_WEBHOOK_SECRET),
},
})
})
app.get('/api/auth/me', async (req: SessionRequest, res) => {
const session = await Session.getSession(req, res, { sessionRequired: false })
if (!session) {
return res.status(401).json({ error: 'unauthorized' })
}
const payload = session.getAccessTokenPayload()
const email = String(payload.email || '')
if (!email) {
return res.status(401).json({ error: 'session_missing_email' })
}
const resolvedAccount = resolveAccountState(email, payload)
if (payload.plan !== resolvedAccount.plan || payload.role !== resolvedAccount.role) {
await session.mergeIntoAccessTokenPayload({
plan: resolvedAccount.plan,
role: resolvedAccount.role,
})
}
return res.json({
user: {
id: session.getUserId(),
email,
name: payload.name || email.split('@')[0],
plan: resolvedAccount.plan,
role: resolvedAccount.role,
can_download: resolvedAccount.canDownload,
billing: resolvedAccount.billing,
auth_method: 'supertokens',
},
mode: 'supertokens',
})
})
app.post('/api/auth/logout', async (req: SessionRequest, res) => {
const session = await Session.getSession(req, res, { sessionRequired: false })
if (session) {
await session.revokeSession()
}
return res.json({ ok: true })
})
app.post('/api/auth/desktop-link', async (req: SessionRequest, res) => {
const origin = String(req.headers.origin || req.headers.referer || '')
if (!matchesAllowedOrigin(origin)) {
return res.status(403).json({ error: 'forbidden_origin' })
}
const session = await Session.getSession(req, res, { sessionRequired: false })
if (!session) {
return res.status(401).json({ error: 'unauthorized' })
}
const payload = session.getAccessTokenPayload()
const email = String(payload.email || '')
if (!email) {
return res.status(401).json({ error: 'session_missing_email' })
}
const resolvedAccount = resolveAccountState(email, payload)
const issueResult = desktopLinkStore.issueToken({
rateLimitKey: `desktop-link:${session.getUserId()}`,
userId: session.getUserId(),
email,
plan: resolvedAccount.plan,
role: resolvedAccount.role,
canDownload: resolvedAccount.canDownload,
accessStatus: resolvedAccount.billing.access_status,
})
if (!issueResult.ok) {
return res.status(429).json({ error: issueResult.error })
}
return res.json({ ok: true, token: issueResult.token, expires_at: issueResult.expiresAt })
})
app.get('/api/auth/desktop-link/verify', (req, res) => {
const token = String(req.query.token || '')
const record = desktopLinkStore.verifyAndConsumeToken(token)
if (!record) {
return res.status(404).json({ error: 'invalid_token' })
}
return res.json({
ok: true,
email: record.email,
plan: record.plan,
role: record.role,
can_download: record.canDownload,
access_status: record.accessStatus,
})
})
app.use(errorHandler())
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error('[hypertwist-auth-server] unhandled error', error)
res.status(500).json({ error: 'internal_server_error' })
})
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}`)
})

View file

@ -0,0 +1,80 @@
import crypto from 'node:crypto'
export type PaddleWebhookVerificationResult =
| { ok: true; timestamp: string }
| {
ok: false
error: 'missing_secret' | 'missing_signature' | 'invalid_signature_header' | 'expired_signature' | 'invalid_signature'
}
interface VerifyPaddleWebhookSignatureInput {
rawBody: string
signatureHeader: string | string[] | undefined
secretKey: string
toleranceMs?: number
now?: () => number
}
function normalizeSignatureHeaderValue(signatureHeader: string | string[] | undefined): string {
if (Array.isArray(signatureHeader)) {
return signatureHeader[0] || ''
}
return String(signatureHeader || '')
}
export function verifyPaddleWebhookSignature({
rawBody,
signatureHeader,
secretKey,
toleranceMs = 5_000,
now = () => Date.now(),
}: VerifyPaddleWebhookSignatureInput): PaddleWebhookVerificationResult {
if (!secretKey.trim()) {
return { ok: false, error: 'missing_secret' }
}
const normalizedHeader = normalizeSignatureHeaderValue(signatureHeader).trim()
if (!normalizedHeader) {
return { ok: false, error: 'missing_signature' }
}
const headerParts = new Map<string, string>()
for (const part of normalizedHeader.split(';')) {
const [key, value] = part.split('=', 2).map((entry) => entry.trim())
if (key && value) {
headerParts.set(key, value)
}
}
const timestamp = headerParts.get('ts')
const providedSignature = headerParts.get('h1')
if (!timestamp || !providedSignature) {
return { ok: false, error: 'invalid_signature_header' }
}
const timestampMs = Number(timestamp) * 1_000
if (!Number.isFinite(timestampMs) || Math.abs(now() - timestampMs) > toleranceMs) {
return { ok: false, error: 'expired_signature' }
}
if (!/^[a-f0-9]+$/i.test(providedSignature) || providedSignature.length % 2 !== 0) {
return { ok: false, error: 'invalid_signature' }
}
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(`${timestamp}:${rawBody}`)
.digest('hex')
const expectedBuffer = Buffer.from(expectedSignature, 'hex')
const providedBuffer = Buffer.from(providedSignature, 'hex')
if (expectedBuffer.length !== providedBuffer.length) {
return { ok: false, error: 'invalid_signature' }
}
if (!crypto.timingSafeEqual(expectedBuffer, providedBuffer)) {
return { ok: false, error: 'invalid_signature' }
}
return { ok: true, timestamp }
}

View file

@ -0,0 +1,158 @@
import crypto from 'node:crypto'
export interface DesktopLinkRecord {
userId: string
email: string
plan: string
role: string
canDownload: boolean
accessStatus: string
createdAt: number
}
interface DesktopLinkRateLimitBucket {
count: number
resetAt: number
}
interface DesktopLinkStoreOptions {
ttlMs: number
rateLimitMax: number
rateLimitWindowMs: number
now?: () => number
randomBytes?: (size: number) => Buffer
}
interface DesktopLinkIssueInput {
rateLimitKey: string
userId: string
email: string
plan: string
role: string
canDownload: boolean
accessStatus: string
}
type DesktopLinkIssueResult =
| { ok: true; token: string; expiresAt: string }
| { ok: false; error: 'rate_limited' }
function normalizeOriginCandidate(candidate: string): string | null {
const trimmed = candidate.trim()
if (!trimmed) {
return null
}
try {
return new URL(trimmed).origin.toLowerCase()
} catch {
return null
}
}
export function buildAllowedOriginMatcher(websiteDomain: string) {
const allowedOrigin = normalizeOriginCandidate(websiteDomain)
if (!allowedOrigin) {
throw new Error(`WEBSITE_DOMAIN '${websiteDomain}' must be an absolute origin.`)
}
return (candidate: string): boolean => normalizeOriginCandidate(candidate) === allowedOrigin
}
export function createDesktopLinkStore({
ttlMs,
rateLimitMax,
rateLimitWindowMs,
now = () => Date.now(),
randomBytes = crypto.randomBytes,
}: DesktopLinkStoreOptions) {
const desktopLinkTokens = new Map<string, DesktopLinkRecord>()
const rateLimitBuckets = new Map<string, DesktopLinkRateLimitBucket>()
function cleanupExpiredTokens() {
const cutoff = now() - ttlMs
for (const [token, record] of desktopLinkTokens) {
if (record.createdAt < cutoff) {
desktopLinkTokens.delete(token)
}
}
for (const [key, bucket] of rateLimitBuckets) {
if (bucket.resetAt <= now()) {
rateLimitBuckets.delete(key)
}
}
}
function isRateLimited(rateLimitKey: string) {
const currentTime = now()
const bucket = rateLimitBuckets.get(rateLimitKey)
if (!bucket || currentTime >= bucket.resetAt) {
rateLimitBuckets.set(rateLimitKey, {
count: 1,
resetAt: currentTime + rateLimitWindowMs,
})
return false
}
bucket.count += 1
return bucket.count > rateLimitMax
}
function issueToken({
rateLimitKey,
userId,
email,
plan,
role,
canDownload,
accessStatus,
}: DesktopLinkIssueInput): DesktopLinkIssueResult {
cleanupExpiredTokens()
if (isRateLimited(rateLimitKey)) {
return { ok: false, error: 'rate_limited' }
}
const createdAt = now()
const token = randomBytes(32).toString('hex')
desktopLinkTokens.set(token, {
userId,
email,
plan,
role,
canDownload,
accessStatus,
createdAt,
})
return {
ok: true,
token,
expiresAt: new Date(createdAt + ttlMs).toISOString(),
}
}
function verifyAndConsumeToken(token: string): DesktopLinkRecord | null {
cleanupExpiredTokens()
const normalizedToken = token.trim()
if (!/^[a-f0-9]{64}$/i.test(normalizedToken)) {
return null
}
const record = desktopLinkTokens.get(normalizedToken)
if (!record) {
return null
}
desktopLinkTokens.delete(normalizedToken)
return record
}
return {
cleanupExpiredTokens,
issueToken,
verifyAndConsumeToken,
getTokenCount: () => desktopLinkTokens.size,
}
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src"]
}

40
website/src/App.tsx Normal file
View file

@ -0,0 +1,40 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import { SuperTokensWrapper } from 'supertokens-auth-react'
import { PlatformAuthProvider } from './auth/platform-auth'
import { isSuperTokensConfigured } from './auth/supertokens-client'
import { AppRouteTree } from './router/AppRouteTree'
import { ROUTER_FUTURE_FLAGS } from './router/router-future'
import './styles/global.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
})
function MaybeSuperTokensWrapper({ children }: { children: React.ReactNode }) {
if (!isSuperTokensConfigured()) {
return <>{children}</>
}
return <SuperTokensWrapper>{children}</SuperTokensWrapper>
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter future={ROUTER_FUTURE_FLAGS}>
<MaybeSuperTokensWrapper>
<PlatformAuthProvider>
<AppRouteTree />
</PlatformAuthProvider>
</MaybeSuperTokensWrapper>
</BrowserRouter>
</QueryClientProvider>
)
}

View file

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { getAuthRuntimeConfig } from '../auth/auth-env'
describe('getAuthRuntimeConfig', () => {
it('normalizes loopback origins to the current browser origin', () => {
const config = getAuthRuntimeConfig({
VITE_SUPERTOKENS_API_DOMAIN: 'http://localhost:3001/',
VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'http://127.0.0.1:4273/',
VITE_AUTH_API_BASE_URL: 'http://localhost:3001/',
}, {
locationOrigin: 'http://127.0.0.1:4273',
})
expect(config.superTokensWebsiteDomain).toBe('http://127.0.0.1:4273')
expect(config.authApiBaseUrl).toBe('http://localhost:3001')
})
})

View file

@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { validateStrongPassword } from '../auth/password-policy'
describe('validateStrongPassword', () => {
it('rejects weak passwords', () => {
expect(validateStrongPassword('weak')).toBeTruthy()
expect(validateStrongPassword('NoSymbols1')).toBeTruthy()
})
it('accepts strong passwords', () => {
expect(validateStrongPassword('HyperTwist!9')).toBeUndefined()
})
})

View file

@ -0,0 +1,181 @@
import { getAuthRuntimeConfig } from './auth-env'
const authRuntime = getAuthRuntimeConfig()
const DEFAULT_AUTH_API_TIMEOUT_MS = authRuntime.authApiTimeoutMs
function normalizeBaseUrl(value: unknown) {
return String(value || '').trim().replace(/\/$/, '')
}
function normalizePath(path: string) {
return path.startsWith('/') ? path : `/${path}`
}
function buildCandidateBaseUrls() {
const configured = normalizeBaseUrl(authRuntime.authApiBaseUrl)
const origin = normalizeBaseUrl(typeof window !== 'undefined' ? window.location.origin : '')
return Array.from(new Set([configured, origin].filter(Boolean)))
}
function buildTimeoutSignal(timeoutMs: number) {
const controller = new AbortController()
const timeout = window.setTimeout(() => controller.abort(), timeoutMs)
return {
signal: controller.signal,
cleanup() {
window.clearTimeout(timeout)
},
}
}
export interface AuthApiFetchResult {
response: Response
baseUrl: string
}
export interface DesktopLinkPayload {
ok: true
token: string
expires_at: string
}
export interface DesktopLinkVerifyPayload {
ok: true
email: string
plan: string
role: string
can_download: boolean
access_status: string
}
export interface AuthHealthPayload {
ok: true
service: string
supertokens: {
configured: boolean
reachable: boolean
ready: boolean
apiVersion?: string | null
error?: string | null
oauth?: {
github: boolean
google: boolean
}
}
fallback: {
enabled: boolean
active?: boolean
reason?: string | null
}
}
export interface ApiBootstrapUserPayload {
user: {
id: string
email: string
name?: string
plan?: string
role?: string
can_download?: boolean
billing?: {
source?: string
access_status?: string
can_download?: boolean
subscription_id?: string | null
customer_id?: string | null
transaction_id?: string | null
last_event_id?: string | null
last_event_type?: string | null
last_event_at?: string | null
updated_at?: string | null
state_path?: string | null
}
is_admin?: boolean
auth_method?: string
}
mode: string
}
export function buildAuthApiBaseUrls() {
return buildCandidateBaseUrls()
}
export async function authApiFetch(
path: string,
init?: RequestInit,
options?: {
timeoutMs?: number
baseUrls?: string[]
},
): Promise<AuthApiFetchResult> {
const baseUrls = options?.baseUrls?.length ? options.baseUrls : buildCandidateBaseUrls()
const timeoutMs = Math.max(1000, options?.timeoutMs ?? DEFAULT_AUTH_API_TIMEOUT_MS)
const normalizedPath = normalizePath(path)
let lastError: unknown = null
for (const baseUrl of baseUrls) {
const { signal, cleanup } = buildTimeoutSignal(timeoutMs)
try {
const response = await fetch(`${baseUrl}${normalizedPath}`, {
credentials: 'include',
...init,
signal,
})
return { response, baseUrl }
} catch (error) {
lastError = error
} finally {
cleanup()
}
}
throw lastError ?? new Error('Auth API unreachable')
}
export async function getCurrentUser() {
const result = await authApiFetch('/api/auth/me')
return result.response
}
export async function logoutCurrentUser() {
const result = await authApiFetch('/api/auth/logout', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
})
return result.response
}
export async function createDesktopLinkToken() {
const result = await authApiFetch('/api/auth/desktop-link', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({}),
})
const payload = await result.response.json() as DesktopLinkPayload | { error?: string }
if (!result.response.ok || payload == null || (payload as DesktopLinkPayload).ok !== true) {
throw new Error((payload as { error?: string }).error || `desktop_link_http_${result.response.status}`)
}
return payload as DesktopLinkPayload
}
export async function verifyDesktopLinkToken(token: string) {
const result = await authApiFetch(`/api/auth/desktop-link/verify?token=${encodeURIComponent(token)}`)
const payload = await result.response.json() as DesktopLinkVerifyPayload | { error?: string }
if (!result.response.ok || payload == null || (payload as DesktopLinkVerifyPayload).ok !== true) {
throw new Error((payload as { error?: string }).error || `desktop_link_verify_http_${result.response.status}`)
}
return payload as DesktopLinkVerifyPayload
}
export async function getAuthHealth() {
const result = await authApiFetch('/api/auth/health')
const payload = await result.response.json() as AuthHealthPayload
if (!result.response.ok) {
throw new Error(`auth_health_http_${result.response.status}`)
}
return payload
}

View file

@ -0,0 +1,92 @@
function normalizeEnvValue(value: unknown) {
return String(value || '').trim().replace(/\/$/, '')
}
function normalizeLoopbackOrigin(configuredOrigin: string, locationOrigin: string) {
if (!configuredOrigin || !locationOrigin) return configuredOrigin
try {
const configured = new URL(configuredOrigin)
const current = new URL(locationOrigin)
const loopbackHosts = new Set(['localhost', '127.0.0.1'])
if (
loopbackHosts.has(configured.hostname)
&& loopbackHosts.has(current.hostname)
&& configured.protocol === current.protocol
&& configured.port === current.port
) {
return normalizeEnvValue(locationOrigin)
}
} catch {
return configuredOrigin
}
return configuredOrigin
}
export interface AuthEnvLike {
[key: string]: string | number | undefined
VITE_SUPERTOKENS_API_DOMAIN?: string
VITE_SUPERTOKENS_WEBSITE_DOMAIN?: string
VITE_SUPERTOKENS_API_BASE_PATH?: string
VITE_SUPERTOKENS_WEBSITE_BASE_PATH?: string
VITE_AUTH_API_BASE_URL?: string
VITE_AUTH_API_TIMEOUT_MS?: string | number
}
export interface AuthRuntimeConfig {
superTokensApiDomain: string
superTokensWebsiteDomain: string
superTokensApiBasePath: string
superTokensWebsiteBasePath: string
authApiBaseUrl: string
authApiTimeoutMs: number
}
export function getAuthRuntimeConfig(
env: Partial<AuthEnvLike> = import.meta.env as Partial<AuthEnvLike>,
options?: { locationOrigin?: string },
): AuthRuntimeConfig {
const locationOrigin = normalizeEnvValue(
options?.locationOrigin ?? (typeof window !== 'undefined' ? window.location.origin : ''),
)
return {
superTokensApiDomain: normalizeLoopbackOrigin(
normalizeEnvValue(env.VITE_SUPERTOKENS_API_DOMAIN),
locationOrigin,
),
superTokensWebsiteDomain: normalizeLoopbackOrigin(
normalizeEnvValue(env.VITE_SUPERTOKENS_WEBSITE_DOMAIN),
locationOrigin,
),
superTokensApiBasePath: normalizeEnvValue(env.VITE_SUPERTOKENS_API_BASE_PATH) || '/auth',
superTokensWebsiteBasePath: normalizeEnvValue(env.VITE_SUPERTOKENS_WEBSITE_BASE_PATH) || '/auth',
authApiBaseUrl: normalizeLoopbackOrigin(
normalizeEnvValue(env.VITE_AUTH_API_BASE_URL),
locationOrigin,
),
authApiTimeoutMs: Math.max(1000, Number(env.VITE_AUTH_API_TIMEOUT_MS || 8000)),
}
}
export function validateAuthRuntimeConfig(
env?: Partial<AuthEnvLike>,
): { ready: boolean; missing: string[]; warnings: string[] } {
const config = getAuthRuntimeConfig(env ?? (import.meta.env as Partial<AuthEnvLike>))
const missing: string[] = []
if (!config.superTokensApiDomain) missing.push('VITE_SUPERTOKENS_API_DOMAIN')
if (!config.superTokensWebsiteDomain) missing.push('VITE_SUPERTOKENS_WEBSITE_DOMAIN')
const warnings: string[] = []
if (!config.authApiBaseUrl) {
warnings.push('VITE_AUTH_API_BASE_URL not configured; same-origin auth fallback remains active.')
}
return {
ready: missing.length === 0,
missing,
warnings,
}
}

View file

@ -0,0 +1,32 @@
const UNSAFE_PASSWORD_SYMBOL_PATTERN = /[\s"'`\\]/
const REQUIRED_UPPERCASE_PATTERN = /[A-Z]/
const REQUIRED_LOWERCASE_PATTERN = /[a-z]/
const REQUIRED_NUMBER_PATTERN = /\d/
const REQUIRED_SYMBOL_PATTERN = /[!@#$%^&*()_\-+=[\]{}|;:,.<>/?~]/
export const PASSWORD_POLICY_HINT =
'8+ chars; upper/lower, number, symbol. Avoid spaces, quotes, backticks, backslashes.'
export const PASSWORD_POLICY_PLACEHOLDER = '8+ chars; upper/lower, number, symbol'
export function validateStrongPassword(password: string) {
const value = String(password || '')
if (value.length < 8) {
return 'Use at least 8 characters.'
}
if (!REQUIRED_UPPERCASE_PATTERN.test(value) || !REQUIRED_LOWERCASE_PATTERN.test(value)) {
return 'Use uppercase and lowercase letters.'
}
if (!REQUIRED_NUMBER_PATTERN.test(value)) {
return 'Add at least one number.'
}
if (!REQUIRED_SYMBOL_PATTERN.test(value)) {
return 'Add at least one symbol.'
}
if (UNSAFE_PASSWORD_SYMBOL_PATTERN.test(value)) {
return 'Avoid spaces, quotes, backticks, and backslashes.'
}
return undefined
}

View file

@ -0,0 +1,386 @@
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} 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 } from './auth-api'
import { ensureSuperTokensInit, isSuperTokensConfigured } from './supertokens-client'
import { validateStrongPassword } from './password-policy'
export type AuthMethod = 'supertokens' | 'email' | 'github' | 'google' | 'orcid'
export type ColorMode = 'dark' | 'light'
export type PlatformRole = 'viewer' | 'operator' | 'reviewer' | 'admin'
export interface PlatformUser {
id: string
name: string
email: string
authMethod: AuthMethod
plan: 'free' | 'operator' | 'studio' | 'enterprise'
role?: PlatformRole
isAdmin?: boolean
canDownload?: boolean
billing?: {
source: string
accessStatus: string
canDownload: boolean
subscriptionId?: string | null
customerId?: string | null
transactionId?: string | null
lastEventId?: string | null
lastEventType?: string | null
lastEventAt?: string | null
updatedAt?: string | null
statePath?: string | null
}
}
export interface PlatformLoginInput {
method: AuthMethod
email: string
password?: string
}
export interface PlatformRegisterInput {
method: 'supertokens' | 'email'
email: string
password: string
name?: string
}
interface PlatformAuthContextValue {
user: PlatformUser | null
isAuthenticated: boolean
isLoading: boolean
colorMode: ColorMode
superTokensConfigured: boolean
login: (input: PlatformLoginInput) => Promise<{ ok: boolean; error?: string }>
register: (input: PlatformRegisterInput) => Promise<{ ok: boolean; error?: string }>
loginWithProvider: (provider: 'github' | 'google' | 'orcid') => Promise<void>
logout: () => Promise<void>
toggleColorMode: () => void
}
const AUTH_STORAGE_KEY = 'hypertwist.platform.user.v1'
const COLOR_MODE_STORAGE_KEY = 'hypertwist.platform.color-mode.v1'
const PlatformAuthContext = createContext<PlatformAuthContextValue | null>(null)
function normalizeEmail(value: unknown) {
return String(value || '').trim().toLowerCase()
}
function deriveNameFromEmail(email: string) {
const userPart = email.split('@')[0] || 'Operator'
return userPart
.replace(/[._-]+/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())
.trim()
}
function normalizePlan(value: unknown): PlatformUser['plan'] {
const plan = String(value || '').trim().toLowerCase()
if (plan === 'operator' || plan === 'studio' || plan === 'enterprise' || plan === 'free') {
return plan
}
if (plan === 'student' || plan === 'team' || plan === 'group' || plan === 'scholar') {
return 'operator'
}
return 'free'
}
function normalizeRole(value: unknown): PlatformRole | undefined {
const role = String(value || '').trim().toLowerCase()
if (role === 'viewer' || role === 'operator' || role === 'reviewer' || role === 'admin') {
return role
}
return undefined
}
function readStoredUser(): PlatformUser | null {
if (typeof window === 'undefined') return null
try {
const raw = window.localStorage.getItem(AUTH_STORAGE_KEY)
if (!raw) return null
const parsed = JSON.parse(raw) as PlatformUser
if (!parsed?.id || !parsed?.email || !parsed?.authMethod) return null
return parsed
} catch {
return null
}
}
function writeStoredUser(user: PlatformUser | null) {
if (typeof window === 'undefined') return
if (!user) {
window.localStorage.removeItem(AUTH_STORAGE_KEY)
return
}
window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(user))
}
function readStoredColorMode(): ColorMode {
if (typeof window === 'undefined') return 'dark'
const stored = window.localStorage.getItem(COLOR_MODE_STORAGE_KEY)
return stored === 'light' ? 'light' : 'dark'
}
function writeStoredColorMode(mode: ColorMode) {
if (typeof window === 'undefined') return
window.localStorage.setItem(COLOR_MODE_STORAGE_KEY, mode)
document.documentElement.dataset.theme = mode
}
function normalizeApiUser(input: ApiBootstrapUserPayload['user'] | undefined, fallbackMethod: AuthMethod): PlatformUser | null {
if (!input) return null
const id = String(input.id || '').trim()
const email = normalizeEmail(input.email)
if (!id || !email) return null
const role = normalizeRole(input.role)
return {
id,
email,
name: String(input.name || '').trim() || deriveNameFromEmail(email),
authMethod: String(input.auth_method || '').trim() === 'supertokens' ? 'supertokens' : fallbackMethod,
plan: normalizePlan(input.plan),
role,
isAdmin: input.is_admin === true || role === 'admin',
canDownload: input.can_download === true,
billing: input.billing ? {
source: String(input.billing.source || 'session'),
accessStatus: String(input.billing.access_status || 'session-default'),
canDownload: input.billing.can_download === true,
subscriptionId: input.billing.subscription_id || null,
customerId: input.billing.customer_id || null,
transactionId: input.billing.transaction_id || null,
lastEventId: input.billing.last_event_id || null,
lastEventType: input.billing.last_event_type || null,
lastEventAt: input.billing.last_event_at || null,
updatedAt: input.billing.updated_at || null,
statePath: input.billing.state_path || null,
} : undefined,
}
}
function createLocalFallbackUser(email: string, password: string, name?: string): PlatformUser {
const safeEmail = normalizeEmail(email)
return {
id: `local-${btoa(`${safeEmail}:${password.length}`)}`,
email: safeEmail,
name: String(name || '').trim() || deriveNameFromEmail(safeEmail),
authMethod: 'email',
plan: 'free',
role: 'operator',
canDownload: false,
billing: {
source: 'local-fallback',
accessStatus: 'local-fallback',
canDownload: false,
},
}
}
async function parseSignInError(response: { status?: string; formFields?: Array<{ error?: string }> }) {
if (response.status === 'WRONG_CREDENTIALS_ERROR') return 'Incorrect email or password.'
if (response.status === 'FIELD_ERROR') return response.formFields?.[0]?.error || 'Check the highlighted field.'
return 'Unable to sign in.'
}
async function parseSignUpError(response: { status?: string; formFields?: Array<{ error?: string }> }) {
if (response.status === 'FIELD_ERROR') return response.formFields?.[0]?.error || 'Check the highlighted field.'
if (response.status === 'SIGN_UP_NOT_ALLOWED') return 'Sign-up is not allowed right now.'
return 'Unable to create your account.'
}
export function PlatformAuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<PlatformUser | null>(() => readStoredUser())
const [isLoading, setIsLoading] = useState(true)
const [colorMode, setColorMode] = useState<ColorMode>(() => readStoredColorMode())
const superTokensConfigured = isSuperTokensConfigured()
useEffect(() => {
writeStoredColorMode(colorMode)
}, [colorMode])
useEffect(() => {
let cancelled = false
async function bootstrap() {
setIsLoading(true)
if (superTokensConfigured) {
ensureSuperTokensInit()
}
try {
const response = await getCurrentUser()
if (!response.ok) {
if (!cancelled) {
setUser(readStoredUser())
}
return
}
const payload = await response.json() as ApiBootstrapUserPayload
const nextUser = normalizeApiUser(payload.user, 'supertokens')
if (!cancelled) {
setUser(nextUser)
writeStoredUser(nextUser)
}
} catch {
if (!cancelled) {
setUser(readStoredUser())
}
} finally {
if (!cancelled) {
setIsLoading(false)
}
}
}
void bootstrap()
return () => {
cancelled = true
}
}, [superTokensConfigured])
const value = useMemo<PlatformAuthContextValue>(() => ({
user,
isAuthenticated: Boolean(user),
isLoading,
colorMode,
superTokensConfigured,
async login(input) {
if (input.method === 'github' || input.method === 'google' || input.method === 'orcid') {
await redirectToThirdPartyLogin({ thirdPartyId: input.method })
return { ok: true }
}
const safeEmail = normalizeEmail(input.email)
if (!safeEmail) {
return { ok: false, error: 'Email is required.' }
}
if (!superTokensConfigured) {
const localUser = createLocalFallbackUser(safeEmail, input.password || '')
setUser(localUser)
writeStoredUser(localUser)
return { ok: true }
}
ensureSuperTokensInit()
const response = await superTokensSignIn({
formFields: [
{ id: 'email', value: safeEmail },
{ id: 'password', value: String(input.password || '') },
],
}) as { status?: string; formFields?: Array<{ error?: string }> }
if (response.status !== 'OK') {
return { ok: false, error: await parseSignInError(response) }
}
try {
const bootstrapResponse = await getCurrentUser()
if (!bootstrapResponse.ok) {
return { ok: false, error: 'Signed in, but account bootstrap failed.' }
}
const payload = await bootstrapResponse.json() as ApiBootstrapUserPayload
const nextUser = normalizeApiUser(payload.user, 'supertokens')
setUser(nextUser)
writeStoredUser(nextUser)
} catch {
return { ok: false, error: 'Signed in, but the account API is unavailable.' }
}
return { ok: true }
},
async register(input) {
const safeEmail = normalizeEmail(input.email)
const passwordError = validateStrongPassword(input.password)
if (!safeEmail) {
return { ok: false, error: 'Email is required.' }
}
if (passwordError) {
return { ok: false, error: passwordError }
}
if (!superTokensConfigured) {
const localUser = createLocalFallbackUser(safeEmail, input.password, input.name)
setUser(localUser)
writeStoredUser(localUser)
return { ok: true }
}
ensureSuperTokensInit()
const response = await superTokensSignUp({
formFields: [
{ id: 'email', value: safeEmail },
{ id: 'password', value: input.password },
],
}) as { status?: string; formFields?: Array<{ error?: string }> }
if (response.status !== 'OK') {
return { ok: false, error: await parseSignUpError(response) }
}
try {
const bootstrapResponse = await getCurrentUser()
if (!bootstrapResponse.ok) {
return { ok: false, error: 'Account created, but bootstrap failed.' }
}
const payload = await bootstrapResponse.json() as ApiBootstrapUserPayload
const nextUser = normalizeApiUser(payload.user, 'supertokens')
setUser(nextUser)
writeStoredUser(nextUser)
} catch {
return { ok: false, error: 'Account created, but the account API is unavailable.' }
}
return { ok: true }
},
async loginWithProvider(provider) {
if (!superTokensConfigured) {
return
}
ensureSuperTokensInit()
await redirectToThirdPartyLogin({ thirdPartyId: provider })
},
async logout() {
writeStoredUser(null)
setUser(null)
if (!superTokensConfigured) {
return
}
try {
await logoutCurrentUser()
} catch {
// Keep local logout deterministic even if the API is unavailable.
}
try {
await superTokensSignOut()
} catch {
// Session revocation can already have happened server-side.
}
},
toggleColorMode() {
setColorMode((current) => current === 'dark' ? 'light' : 'dark')
},
}), [colorMode, isLoading, superTokensConfigured, user])
return <PlatformAuthContext.Provider value={value}>{children}</PlatformAuthContext.Provider>
}
export function usePlatformAuth() {
const context = useContext(PlatformAuthContext)
if (!context) {
throw new Error('usePlatformAuth must be used within PlatformAuthProvider')
}
return context
}

View file

@ -0,0 +1,158 @@
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 { 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'
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
}
const SUPERTOKENS_BRAND_STYLE = `
[data-supertokens~="container"] {
--palette-background: 11, 16, 32;
--palette-inputBackground: 16, 22, 44;
--palette-inputBorder: 84, 203, 255;
--palette-primary: 247, 178, 103;
--palette-primaryBorder: 247, 178, 103;
--palette-success: 158, 255, 199;
--palette-successBackground: 10, 62, 47;
--palette-error: 248, 113, 113;
--palette-errorBackground: 69, 10, 10;
--palette-textTitle: 244, 246, 248;
--palette-textLabel: 212, 225, 245;
--palette-textInput: 247, 249, 252;
--palette-textPrimary: 247, 178, 103;
--palette-textLink: 84, 203, 255;
--palette-buttonText: 11, 16, 32;
--palette-superTokensBrandingBackground: 11, 16, 32;
--palette-superTokensBrandingText: 212, 225, 245;
width: min(460px, calc(100vw - 2rem));
margin-top: 0;
border: 1px solid rgba(84, 203, 255, 0.24) !important;
border-radius: 24px !important;
background: rgba(12, 18, 35, 0.94) !important;
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55) !important;
}
[data-supertokens~="button"] {
min-height: 44px;
border-radius: 999px !important;
background: #f7b267 !important;
border-color: #f7b267 !important;
color: #0b1020 !important;
font-weight: 700;
}
[data-supertokens~="button"]:hover {
background: #ffc788 !important;
}
[data-supertokens~="inputWrapper"] {
min-height: 44px;
border-radius: 14px !important;
background: rgba(14, 21, 40, 0.92) !important;
border: 1px solid rgba(84, 203, 255, 0.28) !important;
color: #f8fafc !important;
}
`
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
}
export 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())
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 || '')),
},
],
},
},
}),
Session.init(),
]
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
}

View file

@ -0,0 +1,74 @@
import { Gauge, KeyRound, MonitorSmartphone, PackageOpen, ShieldCheck } from 'lucide-react'
import { NavLink, Outlet } from 'react-router-dom'
import { usePlatformAuth } from '../../auth/platform-auth'
import { brandConfig } from '../../site-config'
const appLinks = [
{ to: '/app', label: 'Overview', icon: Gauge, end: true },
{ to: '/app/downloads', label: 'Downloads', icon: MonitorSmartphone },
{ to: '/app/browser-access', label: 'Browser Access', icon: PackageOpen },
{ to: '/app/account', label: 'Account', icon: KeyRound },
{ to: '/app/notices', label: 'Notices', icon: ShieldCheck },
] as const
export function AppShell() {
const { user, logout, colorMode, toggleColorMode } = usePlatformAuth()
return (
<div className="app-shell">
<aside className="app-sidebar">
<div className="app-sidebar__brand">
<img
src="/branding/hypertwist-3d-symbol.png"
alt="HyperTwist"
className="app-sidebar__brand-image"
/>
<div>
<strong>{brandConfig.brandName}</strong>
<p>Browser operator shell</p>
</div>
</div>
<nav className="app-nav" aria-label="Operator routes">
{appLinks.map((link) => {
const Icon = link.icon
return (
<NavLink
key={link.to}
to={link.to}
end={link.to === '/app'}
className={({ isActive }) => `app-nav__link${isActive ? ' is-active' : ''}`}
>
<Icon size={18} />
<span>{link.label}</span>
</NavLink>
)
})}
</nav>
<div className="app-sidebar__footer">
<button type="button" className="button button--ghost button--full" onClick={toggleColorMode}>
Switch to {colorMode === 'dark' ? 'light' : 'dark'} mode
</button>
<button type="button" className="button button--ghost button--full" onClick={() => void logout()}>
Log out
</button>
</div>
</aside>
<div className="app-main-shell">
<header className="app-topbar">
<div>
<p className="eyebrow">Operator access</p>
<h1>HyperTwist dashboard</h1>
</div>
<div className="app-topbar__user">
<strong>{user?.name || 'Operator'}</strong>
<span>{user?.plan || 'free'} plan</span>
</div>
</header>
<main className="app-main">
<Outlet />
</main>
</div>
</div>
)
}

View file

@ -0,0 +1,89 @@
import { Link, NavLink } from 'react-router-dom'
import { usePlatformAuth } from '../../auth/platform-auth'
import { brandConfig } from '../../site-config'
import { footerLinks } from '../../site-data'
const marketingLinks = [
{ to: '/', label: 'Home' },
{ to: '/about', label: 'About' },
{ to: '/resources', label: 'Resources' },
{ to: '/pricing', label: 'Pricing' },
{ to: '/download', label: 'Download' },
] as const
export function MarketingShell({
title,
eyebrow,
lede,
children,
}: {
title: string
eyebrow: string
lede: string
children: React.ReactNode
}) {
const { isAuthenticated } = usePlatformAuth()
return (
<div className="site-shell">
<header className="site-header">
<Link to="/" className="brand-mark">
<img
src="/branding/hypertwist-3d-symbol.png"
alt="HyperTwist"
className="brand-mark__image"
/>
<span>
<strong>{brandConfig.brandName}</strong>
<small>{brandConfig.tagline}</small>
</span>
</Link>
<nav className="site-nav" aria-label="Primary">
{marketingLinks.map((link) => (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) => `site-nav__link${isActive ? ' is-active' : ''}`}
>
{link.label}
</NavLink>
))}
</nav>
<div className="site-header__actions">
<Link className="button button--ghost" to={isAuthenticated ? '/app' : '/login'}>
{isAuthenticated ? 'Open dashboard' : 'Log in'}
</Link>
<Link className="button button--primary" to="/download">
Get desktop app
</Link>
</div>
</header>
<main className="page-main">
<section className="page-hero">
<p className="eyebrow">{eyebrow}</p>
<h1>{title}</h1>
<p className="page-hero__lede">{lede}</p>
</section>
{children}
</main>
<footer className="site-footer">
<div className="site-footer__brand">
<strong>{brandConfig.brandName}</strong>
<p>{brandConfig.tagline}</p>
</div>
<div className="site-footer__links">
{footerLinks.map((link) => (
<Link key={link.to} to={link.to}>
{link.label}
</Link>
))}
</div>
<p className="site-footer__note">
Public distribution surfaces must continue to expose open-source notices and corresponding-source guidance when shipped builds contain MPL-covered material.
</p>
</footer>
</div>
)
}

View file

@ -0,0 +1,18 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom'
import { usePlatformAuth } from '../../auth/platform-auth'
import { GeneralPageLoader } from '../ui/Skeletons'
export function ProtectedRoute() {
const { isAuthenticated, isLoading } = usePlatformAuth()
const location = useLocation()
if (isLoading) {
return <GeneralPageLoader title="Loading operator access..." />
}
if (!isAuthenticated) {
return <Navigate to={`/login?next=${encodeURIComponent(location.pathname + location.search)}`} replace />
}
return <Outlet />
}

View file

@ -0,0 +1,36 @@
import { useEffect, useState } from 'react'
import { NamedAgentSpinner } from './agent-spinners'
export function GeneralPageLoader({ title = 'Loading' }: { title?: string }) {
const [isCompleting, setIsCompleting] = useState(false)
useEffect(() => {
const pulseTimer = window.setTimeout(() => {
setIsCompleting(true)
}, 900)
const clearTimer = window.setTimeout(() => {
setIsCompleting(false)
}, 1550)
return () => {
window.clearTimeout(pulseTimer)
window.clearTimeout(clearTimer)
}
}, [])
const spinnerColor = isCompleting ? 'var(--ht-amber)' : 'var(--ht-cyan)'
return (
<div className="general-page-loader-shell" role="status" aria-live="polite" aria-label={title}>
<div className="general-page-loader-frame">
<NamedAgentSpinner
name="cascade"
size={32}
color={spinnerColor}
className={`loading-spinner ${isCompleting ? 'is-completing' : ''}`}
/>
<p className="general-page-loader-text">{title}</p>
</div>
</div>
)
}

View file

@ -0,0 +1,81 @@
import { useEffect, useState, type CSSProperties } from 'react'
const spinnerRegistry = {
cascade: {
frames: [
'',
'⠁⠀⠀⠀',
'⠋⠀⠀⠀',
'⠞⠁⠀⠀',
'⡴⠋⠀⠀',
'⣠⠞⠁⠀',
'⢀⡴⠋⠀',
'⠀⣠⠞⠁',
'⠀⢀⡴⠋',
'⠀⠀⣠⠞',
'⠀⠀⢀⡴',
'⠀⠀⠀⣠',
'⠀⠀⠀⢀',
],
interval: 60,
},
orbit: {
frames: ['⠃', '⠉', '⠘', '⠰', '⢠', '⣀', '⡄', '⠆'],
interval: 90,
},
} as const
function useSpinnerFrame(frames: readonly string[], interval: number) {
const [index, setIndex] = useState(0)
useEffect(() => {
const id = window.setInterval(() => {
setIndex((current) => (current + 1) % frames.length)
}, interval)
return () => window.clearInterval(id)
}, [frames, interval])
return frames[index] ?? ''
}
export function NamedAgentSpinner({
name,
size = 28,
color = 'currentColor',
className,
style,
}: {
name: keyof typeof spinnerRegistry
size?: number
color?: string
className?: string
style?: CSSProperties
}) {
const meta = spinnerRegistry[name]
const frame = useSpinnerFrame(meta.frames, meta.interval)
return (
<div
className={className}
style={{
width: size * 2.5,
height: size * 2.5,
display: 'grid',
placeItems: 'center',
...style,
}}
>
<span
style={{
fontSize: size,
color,
lineHeight: 1,
whiteSpace: 'pre',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
}}
>
{frame}
</span>
</div>
)
}

9
website/src/main.tsx Normal file
View file

@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View file

@ -0,0 +1,207 @@
import { useMemo } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { buildAuthApiBaseUrls, createDesktopLinkToken, getAuthHealth } from '../auth/auth-api'
import { usePlatformAuth } from '../auth/platform-auth'
import { downloadTargets, mplSourceUrl } from '../site-config'
import { roadmapHonestyCards } from '../site-data'
function Panel({
title,
children,
kicker,
}: {
title: string
children: React.ReactNode
kicker?: string
}) {
return (
<article className="panel">
{kicker ? <p className="eyebrow">{kicker}</p> : null}
<h2>{title}</h2>
{children}
</article>
)
}
export function DashboardOverviewPage() {
const { user } = usePlatformAuth()
const healthQuery = useQuery({
queryKey: ['auth-health'],
queryFn: getAuthHealth,
})
const desktopLinkMutation = useMutation({
mutationFn: createDesktopLinkToken,
})
const verifyUrl = useMemo(() => {
const token = desktopLinkMutation.data?.token
if (!token) return ''
const [baseUrl] = buildAuthApiBaseUrls()
if (!baseUrl) return ''
return `${baseUrl}/api/auth/desktop-link/verify?token=${encodeURIComponent(token)}`
}, [desktopLinkMutation.data?.token])
return (
<div className="panel-grid">
<Panel title="Account state" kicker="Live session">
<p><strong>{user?.name}</strong></p>
<p>{user?.email}</p>
<p>Plan: {user?.plan}</p>
<p>Role: {user?.role || 'operator'}</p>
<p>Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}</p>
</Panel>
<Panel title="Auth and server health" kicker="Browser shell">
{healthQuery.isLoading ? <p>Checking auth server health...</p> : null}
{healthQuery.isError ? <p className="form-error">Auth health probe failed. The local fallback can still keep the dashboard usable.</p> : null}
{healthQuery.data ? (
<ul className="list">
<li>Service: {healthQuery.data.service}</li>
<li>SuperTokens configured: {healthQuery.data.supertokens.configured ? 'yes' : 'no'}</li>
<li>SuperTokens core reachable: {healthQuery.data.supertokens.reachable ? 'yes' : 'no'}</li>
<li>SuperTokens ready: {healthQuery.data.supertokens.ready ? 'yes' : 'no'}</li>
<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>
</ul>
) : null}
{healthQuery.data?.fallback.active ? (
<p className="form-error">
Shared auth core is not fully ready right now. Dashboard fallback posture remains available{healthQuery.data.fallback.reason ? ` (${healthQuery.data.fallback.reason})` : ''}.
</p>
) : null}
</Panel>
<Panel title="Desktop-link pairing" kicker="Browser to desktop handoff">
<p>Generate a short-lived token so the local desktop runtime can verify your browser session without exposing account credentials.</p>
<button
className="button button--primary"
type="button"
onClick={() => desktopLinkMutation.mutate()}
disabled={desktopLinkMutation.isPending}
>
{desktopLinkMutation.isPending ? 'Generating token...' : 'Generate desktop-link token'}
</button>
{desktopLinkMutation.isError ? (
<p className="form-error">Could not generate a token. Ensure the auth server is reachable and your session is valid.</p>
) : null}
{desktopLinkMutation.data ? (
<div className="code-block top-gap">
<p>Token: <code>{desktopLinkMutation.data.token}</code></p>
<p>Expires: {desktopLinkMutation.data.expires_at}</p>
<p>Verify URL: <code>{verifyUrl || 'same-origin verify route'}</code></p>
</div>
) : null}
</Panel>
<Panel title="Billing and entitlement state" kicker="Release access">
<ul className="list">
<li>Source: {user?.billing?.source || 'session'}</li>
<li>Access status: {user?.billing?.accessStatus || 'session-default'}</li>
<li>Desktop download access: {user?.billing?.canDownload ? 'enabled' : 'not yet enabled'}</li>
<li>Last billing event: {user?.billing?.lastEventType || 'none recorded'}</li>
</ul>
</Panel>
<Panel title="Product boundary" kicker="Roadmap honesty">
<ul className="list">
{roadmapHonestyCards.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</Panel>
</div>
)
}
export function DownloadCenterPage() {
const { user } = usePlatformAuth()
const canDownload = user?.canDownload === true
return (
<div className="panel-grid">
<Panel title="Release targets" kicker="Desktop distribution">
{!canDownload ? (
<p className="form-error">
Your current account does not yet have desktop download entitlement.
Complete the matching checkout or operator provisioning step, then refresh this dashboard.
</p>
) : null}
<div className="card-grid">
{downloadTargets.map((target) => (
<article key={target.platform} className="card card--compact">
<h3>{target.platform}</h3>
<p>{target.details}</p>
{target.href && canDownload ? (
<a className="button button--primary button--full" href={target.href}>
Download {target.platform}
</a>
) : (
<div className="button button--ghost button--full is-disabled" aria-disabled="true">
{target.href ? 'Account not entitled yet' : 'Release URL not configured yet'}
</div>
)}
</article>
))}
</div>
</Panel>
</div>
)
}
export function BrowserAccessPage() {
return (
<div className="panel-grid">
<Panel title="Current browser posture" kicker="Shipping lane">
<ul className="list">
<li>The embedded browser/CEF shell is live and owned as part of the current shipping posture.</li>
<li>Browser runtime status, runtime-ready typing, and native operator diagnostics are already surfaced in the desktop runtime.</li>
<li>The public browser dashboard is for operator/account/release access, not a claim of full simulator parity.</li>
</ul>
</Panel>
<Panel title="Optional branches remain bounded" kicker="Not widened here">
<ul className="list">
<li>The optional full-browser client remains spec-only.</li>
<li>MagicTile native renderer widening remains explicit No-Go until a real browser-host gap is proven.</li>
<li>This dashboard intentionally stays above those boundaries instead of silently crossing them.</li>
</ul>
</Panel>
</div>
)
}
export function AccountPage() {
const { user, superTokensConfigured } = usePlatformAuth()
return (
<div className="panel-grid">
<Panel title="Session profile" kicker="Account details">
<ul className="list">
<li>Email: {user?.email}</li>
<li>Name: {user?.name}</li>
<li>Plan: {user?.plan}</li>
<li>Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}</li>
<li>Billing status: {user?.billing?.accessStatus || 'session-default'}</li>
<li>Auth stack: {superTokensConfigured ? 'SuperTokens-backed' : 'Local fallback mode'}</li>
</ul>
</Panel>
</div>
)
}
export function NoticesPage() {
return (
<div className="panel-grid">
<Panel title="Distribution notices" kicker="Legal posture">
<p>
Pricing, checkout, and download surfaces must expose open-source notices and corresponding-source guidance whenever shipped builds contain MPL-covered material.
</p>
<p>
Current corresponding-source URL: {mplSourceUrl || 'configure VITE_MPL_SOURCE_URL before public launch'}
</p>
</Panel>
</div>
)
}

View file

@ -0,0 +1,150 @@
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'
function AuthShell({
title,
subtitle,
children,
footer,
}: {
title: string
subtitle: string
children: React.ReactNode
footer: React.ReactNode
}) {
return (
<div className="auth-shell">
<div className="auth-card">
<p className="eyebrow">Browser account access</p>
<h1>{title}</h1>
<p className="auth-card__subtitle">{subtitle}</p>
{children}
<div className="auth-card__footer">{footer}</div>
</div>
</div>
)
}
function useNextPath() {
const [searchParams] = useSearchParams()
const next = searchParams.get('next') || '/app'
return next.startsWith('/') ? next : '/app'
}
export function LoginPage() {
const navigate = useNavigate()
const { login, isAuthenticated } = usePlatformAuth()
const nextPath = useNextPath()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
if (isAuthenticated) {
navigate(nextPath, { replace: true })
}
}, [isAuthenticated, navigate, nextPath])
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsSubmitting(true)
setError('')
const result = await login({ method: 'email', email, password })
setIsSubmitting(false)
if (!result.ok) {
setError(result.error || 'Unable to sign in.')
return
}
navigate(nextPath, { replace: true })
}
return (
<AuthShell
title="Log in to HyperTwist"
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>}
>
<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)} />
<label className="input-label" htmlFor="login-password">Password</label>
<input id="login-password" className="input" type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
{error ? <p className="form-error">{error}</p> : null}
<button className="button button--primary button--full" disabled={isSubmitting} type="submit">
{isSubmitting ? 'Signing in...' : 'Log in'}
</button>
</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>
) : null}
{isGitHubOAuthEnabled() ? (
<button className="button button--ghost button--full" type="button" onClick={() => void login({ method: 'github', email: '' })}>
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>
) : null}
</div>
</AuthShell>
)
}
export function RegisterPage() {
const navigate = useNavigate()
const { 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)
useEffect(() => {
if (isAuthenticated) {
navigate(nextPath, { replace: true })
}
}, [isAuthenticated, navigate, nextPath])
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsSubmitting(true)
setError('')
const result = await register({ method: 'email', email, password, name })
setIsSubmitting(false)
if (!result.ok) {
setError(result.error || 'Unable to create your account.')
return
}
navigate(nextPath, { replace: true })
}
return (
<AuthShell
title="Create a HyperTwist account"
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>}
>
<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)} />
<label className="input-label" htmlFor="register-email">Email</label>
<input id="register-email" className="input" value={email} onChange={(event) => setEmail(event.target.value)} />
<label className="input-label" htmlFor="register-password">Password</label>
<input id="register-password" className="input" type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
{error ? <p className="form-error">{error}</p> : null}
<button className="button button--primary button--full" disabled={isSubmitting} type="submit">
{isSubmitting ? 'Creating account...' : 'Create account'}
</button>
</form>
</AuthShell>
)
}

View file

@ -0,0 +1,579 @@
import { useDeferredValue, useMemo, useState } from 'react'
import { ArrowRight, BookOpenText, Boxes, Download, ExternalLink, Landmark, MonitorCog, Sparkles } from 'lucide-react'
import { Link } from 'react-router-dom'
import { MarketingShell } from '../components/layout/MarketingShell'
import {
brandConfig,
downloadTargets,
paddleReadyDescription,
planCatalog,
publicDocsUrl,
releaseNotesUrl,
} from '../site-config'
import {
capabilityPillars,
changelogEntries,
companyNarrative,
heroMetrics,
openSourceNotices,
resourceCollections,
roadmapHonestyCards,
shippingNowCards,
sourceAvailability,
supportFaqs,
} from '../site-data'
function Section({
title,
description,
children,
}: {
title: string
description?: string
children: React.ReactNode
}) {
return (
<section className="page-section">
<div className="section-heading">
<h2>{title}</h2>
{description ? <p>{description}</p> : null}
</div>
{children}
</section>
)
}
export function HomeLanding() {
return (
<MarketingShell
eyebrow="Desktop-first training. Browser-first operator access."
title="HyperTwist turns cube practice into a real operator-grade training stack."
lede="Recognition, replay, coaching, analytics, higher-dimensional runtime ownership, public distribution, and browser-based account access all live under one honest product story."
>
<section className="hero-panel">
<div className="hero-grid">
<div className="hero-copy">
<p>
HyperTwist is a native training environment for classic cube, higher-dimensional
families, browser-assisted recognition, replay explanation, and desktop packaging.
The public site is intentionally honest: the desktop runtime is real, the browser
account/dashboard is real, and the optional full-browser simulator path remains spec-only.
</p>
<div className="button-row">
<Link className="button button--primary" to="/download">
Download desktop app
</Link>
<Link className="button button--ghost" to="/app">
Open operator dashboard
</Link>
<Link className="button button--ghost" to="/pricing">
View pricing
</Link>
</div>
</div>
<div className="hero-visual-card">
<img
src="/branding/hypertwist-3d-symbol.png"
alt="HyperTwist symbol"
className="hero-visual-card__image"
/>
<p className="hero-visual-card__caption">
Browser account shell outside the simulator. Native Unreal runtime inside the simulator.
</p>
</div>
</div>
<div className="metric-grid">
{heroMetrics.map((metric) => (
<article key={metric.label} className="metric-card">
<strong>{metric.value}</strong>
<span>{metric.label}</span>
</article>
))}
</div>
</section>
<Section
title="What ships now"
description="The public site only describes current product truth or explicitly marked retained/spec-only branches."
>
<div className="card-grid">
{shippingNowCards.map((item) => (
<article key={item} className="card card--compact">
<Sparkles size={18} />
<p>{item}</p>
</article>
))}
</div>
</Section>
<Section
title="Capability pillars"
description="The page structure borrows the FamiliarOS and ScriptoriumAI public-shell discipline, then rebrands the actual product around HyperTwist runtime authority."
>
<div className="card-grid">
{capabilityPillars.map((pillar) => (
<article key={pillar.title} className="card">
<h3>{pillar.title}</h3>
<p>{pillar.description}</p>
</article>
))}
</div>
</Section>
<Section
title="Roadmap-honest posture"
description="Important boundaries stay visible instead of being blurred into vague marketing claims."
>
<div className="split-grid">
{roadmapHonestyCards.map((item) => (
<article key={item} className="callout">
<p>{item}</p>
</article>
))}
</div>
</Section>
<Section
title="Delivery surfaces"
description="Use the website for account, release, pricing, and notices. Use the desktop runtime for the core simulator."
>
<div className="feature-band">
<article className="feature-band__card">
<MonitorCog size={22} />
<h3>Browser account and operator shell</h3>
<p>Authenticated browser access for release posture, desktop pairing, notices, and operator state.</p>
<Link to="/app" className="inline-link">
Open dashboard <ArrowRight size={15} />
</Link>
</article>
<article className="feature-band__card">
<Download size={22} />
<h3>Desktop download and package lane</h3>
<p>Public download posture for the native Unreal build, with legal linkage already wired in.</p>
<Link to="/download" className="inline-link">
Open download center <ArrowRight size={15} />
</Link>
</article>
<article className="feature-band__card">
<Landmark size={22} />
<h3>Checkout and notices discipline</h3>
<p>Paddle-ready pricing plus public notices surfaces for any downloadable build containing MPL-covered material.</p>
<Link to="/open-source-notices" className="inline-link">
Review notices <ArrowRight size={15} />
</Link>
</article>
</div>
</Section>
</MarketingShell>
)
}
export function AboutPage() {
return (
<MarketingShell
eyebrow="Why HyperTwist exists"
title="A training stack serious enough for higher-dimensional cubing."
lede="HyperTwist exists because cubers deserve one coherent system for physical recognition, explanation, practice, replay, analytics, and hyper puzzle ownership."
>
<Section title="Mission">
<div className="card">
<p>{companyNarrative.mission}</p>
<p>{companyNarrative.posture}</p>
<p>{companyNarrative.distribution}</p>
</div>
</Section>
<Section title="What makes the product different">
<div className="card-grid">
<article className="card">
<Boxes size={22} />
<h3>It treats higher-dimensional puzzles as first-class work</h3>
<p>120-cell and 5D runtime ownership are not hand-wavy aspirations. They are part of the current product truth.</p>
</article>
<article className="card">
<BookOpenText size={22} />
<h3>It stays roadmap-honest</h3>
<p>Shipped, retained, and spec-only surfaces remain clearly separated so public copy matches actual authority.</p>
</article>
<article className="card">
<MonitorCog size={22} />
<h3>It separates browser shell from simulator truth</h3>
<p>The public web surface helps operators access the product without pretending the browser already replaces the desktop runtime.</p>
</article>
</div>
</Section>
</MarketingShell>
)
}
export function ResourcesPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const filteredCollections = useMemo(() => {
const normalized = deferredQuery.trim().toLowerCase()
if (!normalized) return resourceCollections
return resourceCollections
.map((collection) => ({
...collection,
items: collection.items.filter((item) => item.toLowerCase().includes(normalized) || collection.title.toLowerCase().includes(normalized)),
}))
.filter((collection) => collection.items.length > 0)
}, [deferredQuery])
return (
<MarketingShell
eyebrow="Public resources"
title="Resources that explain the product without leaking operator-only internals."
lede="This surface is shaped after the FamiliarOS and ScriptoriumAI public resource pages, but constrained to safe HyperTwist product-facing material."
>
<Section title="Resource finder" description="Search the public resource categories that matter to operators and buyers.">
<label className="input-label" htmlFor="resource-query">Filter resources</label>
<input
id="resource-query"
className="input"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search training, rollout, notices..."
/>
<div className="card-grid top-gap">
{filteredCollections.map((collection) => (
<article key={collection.title} className="card">
<h3>{collection.title}</h3>
<ul className="list">
{collection.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</article>
))}
</div>
</Section>
<Section title="Direct routes">
<div className="feature-band">
<Link to="/docs" className="feature-band__card feature-band__card--link">
<BookOpenText size={22} />
<h3>Docs landing</h3>
<p>Product-facing documentation, boundaries, and rollout guidance.</p>
</Link>
<Link to="/support" className="feature-band__card feature-band__card--link">
<ExternalLink size={22} />
<h3>Support</h3>
<p>Contact, rollout questions, and account/download help.</p>
</Link>
<Link to="/changelog" className="feature-band__card feature-band__card--link">
<Sparkles size={22} />
<h3>Release notes</h3>
<p>Recent public-facing packets and posture updates.</p>
</Link>
</div>
</Section>
</MarketingShell>
)
}
export function DocsPage() {
return (
<MarketingShell
eyebrow="Documentation posture"
title="HyperTwist documentation stays capability-accurate."
lede="The public docs surface points users to product-safe truth: feature registry discipline, roadmap honesty, release notes, and distribution/legal guidance."
>
<Section title="Public documentation lanes">
<div className="card-grid">
<article className="card">
<h3>Feature truth</h3>
<p>Use documentation derived from the feature registry so shipped capability, retained capability, and spec-only branches are not mixed together.</p>
</article>
<article className="card">
<h3>Roadmap truth</h3>
<p>Roadmap-facing copy should say the embedded browser shell is live, while the optional full-browser client remains separate and frozen as spec-only.</p>
</article>
<article className="card">
<h3>Distribution truth</h3>
<p>Pricing, checkout, and download docs must stay linked to open-source notices whenever shipped builds contain MPL-covered material.</p>
</article>
</div>
</Section>
{publicDocsUrl ? (
<Section title="External docs portal">
<a className="button button--primary" href={publicDocsUrl} target="_blank" rel="noreferrer">
Open public docs portal
</a>
</Section>
) : null}
</MarketingShell>
)
}
export function SupportPage() {
return (
<MarketingShell
eyebrow="Operator help"
title="Support for rollout, downloads, pricing, and browser-to-desktop access."
lede="Support is focused on helping operators understand what the browser shell does, what the desktop build does, and how the two connect."
>
<Section title="Contact">
<article className="card">
<p>
Email <a href={brandConfig.contact.emailHref}>{brandConfig.contact.email}</a> for account activation,
release enablement, pricing configuration, or deployment help.
</p>
</article>
</Section>
<Section title="Frequently asked questions">
<div className="card-grid">
{supportFaqs.map((faq) => (
<article className="card" key={faq.question}>
<h3>{faq.question}</h3>
<p>{faq.answer}</p>
</article>
))}
</div>
</Section>
</MarketingShell>
)
}
export function ChangelogPage() {
return (
<MarketingShell
eyebrow="Release notes"
title="Recent public-facing HyperTwist changes."
lede="This page focuses on user-visible posture changes: browser operator access, package hardening, diagnostics, and distribution readiness."
>
<Section title="Latest changes">
<div className="timeline">
{changelogEntries.map((entry) => (
<article key={`${entry.date}-${entry.title}`} className="timeline-entry">
<p className="timeline-entry__date">{entry.date}</p>
<h3>{entry.title}</h3>
<p>{entry.details}</p>
</article>
))}
</div>
</Section>
{releaseNotesUrl ? (
<Section title="External release notes">
<a className="button button--ghost" href={releaseNotesUrl} target="_blank" rel="noreferrer">
Open release feed
</a>
</Section>
) : null}
</MarketingShell>
)
}
export function PricingPage() {
return (
<MarketingShell
eyebrow="Paddle-ready plans"
title="Pricing that matches the actual delivery model."
lede="The browser shell handles account, release, and billing access. The simulator remains desktop-first. Prices and checkouts can be switched live through the same Paddle-ready structure used across the broader product family."
>
<Section title="Plan lineup" description={paddleReadyDescription}>
<div className="card-grid card-grid--pricing">
{planCatalog.map((plan) => (
<article key={plan.key} className="card card--pricing">
<p className="status-pill">{plan.name}</p>
<h3>{plan.price}</h3>
<p>{plan.notes}</p>
<ul className="list">
{plan.features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
<a className="button button--primary button--full" href={plan.ctaHref}>
{plan.ctaLabel}
</a>
</article>
))}
</div>
</Section>
<Section title="Important launch note">
<article className="callout">
<p>
Public pricing, checkout, and download pages are distribution surfaces. Before external launch,
keep their legal footer and open-source notices link live and ensure the corresponding-source URL is configured for any downloadable build containing MPL-covered material.
</p>
</article>
</Section>
</MarketingShell>
)
}
export function DownloadPage() {
return (
<MarketingShell
eyebrow="Desktop distribution"
title="Download the desktop build and pair it with your browser account."
lede="The website provides account, release, and legal surfaces. The actual simulator ships through the desktop lane, with package validation and release discipline carried over from the HyperTwist roadmap."
>
<Section title="Available targets">
<div className="card-grid">
{downloadTargets.map((target) => (
<article key={target.platform} className="card">
<h3>{target.platform}</h3>
<p className="status-pill">{target.subtitle}</p>
<p>{target.details}</p>
{target.href ? (
<Link className="button button--primary button--full" to="/app/downloads">
Sign in for {target.platform} access
</Link>
) : (
<div className="button button--ghost button--full is-disabled" aria-disabled="true">
Release URL not configured yet
</div>
)}
</article>
))}
</div>
</Section>
<Section title="Why this page does not expose raw download URLs">
<article className="callout">
<p>
HyperTwist treats desktop distribution as an account-gated release surface.
Public pages can describe supported targets and release posture, but the actual
download links live behind the protected dashboard where plan and entitlement
state are resolved.
</p>
<Link className="button button--ghost" to="/login">
Sign in to check access
</Link>
</article>
</Section>
<Section title="Pair the desktop app with your browser account">
<article className="callout">
<p>
After sign-in, open the operator dashboard to generate a desktop-link token.
That token is designed to hand browser identity and plan posture over to the local desktop app without exposing your password.
</p>
<Link className="button button--ghost" to="/app">
Open dashboard
</Link>
</article>
</Section>
</MarketingShell>
)
}
export function OpenSourceNoticesPage() {
return (
<MarketingShell
eyebrow="Legal and notices"
title="Open-source notices for public distribution surfaces."
lede="HyperTwist pricing, checkout, release, and download pages must make legal and corresponding-source guidance visible whenever shipped builds include MPL-covered material."
>
<Section title="Key components">
<div className="card-grid">
{openSourceNotices.map((item) => (
<article className="card" key={item.component}>
<h3>{item.component}</h3>
<p className="status-pill">{item.license}</p>
<p>{item.whyItMatters}</p>
</article>
))}
</div>
</Section>
<Section title="Corresponding source">
<div className="card">
<p>
MPL-covered shipped builds need a stable corresponding-source location for the exact distributed material.
</p>
<ul className="list">
<li>
Public corresponding-source URL:{' '}
{sourceAvailability.mplSourceUrl ? (
<a href={sourceAvailability.mplSourceUrl} target="_blank" rel="noreferrer">{sourceAvailability.mplSourceUrl}</a>
) : (
'configure VITE_MPL_SOURCE_URL before public launch'
)}
</li>
<li>
Public repository / notices reference:{' '}
{sourceAvailability.openSourceRepoUrl ? (
<a href={sourceAvailability.openSourceRepoUrl} target="_blank" rel="noreferrer">{sourceAvailability.openSourceRepoUrl}</a>
) : (
'configure VITE_OPEN_SOURCE_REPO_URL before public launch'
)}
</li>
</ul>
<p>
Official MPL 2.0 license text:{' '}
<a href="https://www.mozilla.org/en-US/MPL/2.0/" target="_blank" rel="noreferrer">
https://www.mozilla.org/en-US/MPL/2.0/
</a>
</p>
</div>
</Section>
</MarketingShell>
)
}
export function PrivacyPage() {
return (
<MarketingShell
eyebrow="Privacy"
title="Privacy posture"
lede="HyperTwist keeps the browser shell narrow and the simulator desktop-first. Privacy descriptions must reflect that separation clearly."
>
<Section title="Core points">
<article className="card">
<ul className="list">
<li>The public website stores account/session data needed for authentication, plan access, and desktop-link issuance.</li>
<li>The browser shell does not claim ownership over the full simulator runtime state unless a future browser-client packet is explicitly opened.</li>
<li>Support, billing, and release operations should collect only the data required to deliver digital access and maintain legal compliance.</li>
</ul>
</article>
</Section>
</MarketingShell>
)
}
export function TermsPage() {
return (
<MarketingShell
eyebrow="Terms"
title="Terms of access"
lede="HyperTwist access is digital-first and plan-gated. Terms should match the actual delivery and account model."
>
<Section title="Service posture">
<article className="card">
<ul className="list">
<li>Browser access covers public pages, account, release, download, and operator/dashboard surfaces.</li>
<li>The simulator itself is delivered through the desktop lane unless a later browser-client branch is explicitly opened.</li>
<li>Downloaded builds and their public distribution pages remain subject to open-source notice and corresponding-source disclosure rules where applicable.</li>
</ul>
</article>
</Section>
</MarketingShell>
)
}
export function ShippingPaymentPage() {
return (
<MarketingShell
eyebrow="Shipping & payment"
title="Digital delivery only"
lede="HyperTwist is not a physical-goods storefront. Delivery happens through authenticated account access, release pages, and desktop downloads."
>
<Section title="Delivery model">
<article className="card">
<p>{paddleReadyDescription}</p>
<ul className="list">
<li>No physical goods ship through this site.</li>
<li>Pricing and checkout are structured for Paddle-backed digital plans.</li>
<li>Desktop downloads must remain paired with public notices and legal links when required by shipped-code obligations.</li>
</ul>
</article>
</Section>
</MarketingShell>
)
}

View file

@ -0,0 +1,36 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { AppShell } from '../components/layout/AppShell'
import { ProtectedRoute } from '../components/routes/ProtectedRoute'
import { renderPublicRoutes } from './PublicRoutes'
import { Suspense, lazy } from 'react'
import { GeneralPageLoader } from '../components/ui/Skeletons'
const DashboardOverviewPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.DashboardOverviewPage })))
const DownloadCenterPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.DownloadCenterPage })))
const BrowserAccessPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.BrowserAccessPage })))
const AccountPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.AccountPage })))
const NoticesPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.NoticesPage })))
function withAppLoader(title: string, element: React.ReactNode) {
return <Suspense fallback={<GeneralPageLoader title={title} />}>{element}</Suspense>
}
export function AppRouteTree() {
return (
<Routes>
{renderPublicRoutes()}
<Route element={<ProtectedRoute />}>
<Route path="/app" element={<AppShell />}>
<Route index element={withAppLoader('Loading dashboard...', <DashboardOverviewPage />)} />
<Route path="downloads" element={withAppLoader('Loading downloads...', <DownloadCenterPage />)} />
<Route path="browser-access" element={withAppLoader('Loading browser access...', <BrowserAccessPage />)} />
<Route path="account" element={withAppLoader('Loading account...', <AccountPage />)} />
<Route path="notices" element={withAppLoader('Loading notices...', <NoticesPage />)} />
</Route>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
}

View file

@ -0,0 +1,43 @@
import { Suspense, lazy, type ReactNode } from 'react'
import { Route } from 'react-router-dom'
import { GeneralPageLoader } from '../components/ui/Skeletons'
const HomeLanding = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.HomeLanding })))
const AboutPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.AboutPage })))
const ResourcesPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.ResourcesPage })))
const DocsPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.DocsPage })))
const SupportPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.SupportPage })))
const ChangelogPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.ChangelogPage })))
const PricingPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.PricingPage })))
const DownloadPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.DownloadPage })))
const OpenSourceNoticesPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.OpenSourceNoticesPage })))
const PrivacyPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.PrivacyPage })))
const TermsPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.TermsPage })))
const ShippingPaymentPage = lazy(() => import('../pages/public-pages').then((m) => ({ default: m.ShippingPaymentPage })))
const LoginPage = lazy(() => import('../pages/auth-pages').then((m) => ({ default: m.LoginPage })))
const RegisterPage = lazy(() => import('../pages/auth-pages').then((m) => ({ default: m.RegisterPage })))
function withGeneralLoader(title: string, element: ReactNode) {
return <Suspense fallback={<GeneralPageLoader title={title} />}>{element}</Suspense>
}
export function renderPublicRoutes() {
return (
<>
<Route path="/" element={withGeneralLoader('Loading HyperTwist...', <HomeLanding />)} />
<Route path="/about" element={withGeneralLoader('About HyperTwist', <AboutPage />)} />
<Route path="/resources" element={withGeneralLoader('Resources', <ResourcesPage />)} />
<Route path="/docs" element={withGeneralLoader('Documentation', <DocsPage />)} />
<Route path="/support" element={withGeneralLoader('Support', <SupportPage />)} />
<Route path="/changelog" element={withGeneralLoader('Release notes', <ChangelogPage />)} />
<Route path="/pricing" element={withGeneralLoader('Pricing', <PricingPage />)} />
<Route path="/download" element={withGeneralLoader('Download center', <DownloadPage />)} />
<Route path="/open-source-notices" element={withGeneralLoader('Open source notices', <OpenSourceNoticesPage />)} />
<Route path="/privacy" element={withGeneralLoader('Privacy', <PrivacyPage />)} />
<Route path="/terms" element={withGeneralLoader('Terms', <TermsPage />)} />
<Route path="/shipping-payment" element={withGeneralLoader('Shipping & payment', <ShippingPaymentPage />)} />
<Route path="/login" element={withGeneralLoader('Log in', <LoginPage />)} />
<Route path="/register" element={withGeneralLoader('Create account', <RegisterPage />)} />
</>
)
}

View file

@ -0,0 +1,4 @@
export const ROUTER_FUTURE_FLAGS = {
v7_startTransition: true,
v7_relativeSplatPath: true,
} as const

View file

@ -0,0 +1,93 @@
const env = import.meta.env as Record<string, string | undefined>
function readTrimmedEnv(name: string, fallback = '') {
return String(env[name] || fallback).trim()
}
export const brandConfig = {
brandName: 'HyperTwist',
legalName: 'HyperTwist',
domain: 'hypertwist.app',
tagline: 'Native cube and hypercube training, from first solve to 120-cell.',
contact: {
email: readTrimmedEnv('VITE_SUPPORT_EMAIL', 'hello@hypertwist.app'),
emailHref: `mailto:${readTrimmedEnv('VITE_SUPPORT_EMAIL', 'hello@hypertwist.app')}`,
},
payment: {
provider: 'Paddle',
},
}
export const planCatalog = [
{
key: 'explorer',
name: 'Explorer',
price: 'Free',
ctaLabel: 'Start in the browser',
ctaHref: '/register',
notes: 'Preview the browser account shell, operator dashboard, release notes, and desktop onboarding posture.',
features: [
'Browser account access',
'Release notes and resource center',
'Operator dashboard preview',
'Open-source notices and distribution transparency',
],
},
{
key: 'operator',
name: 'Operator',
price: readTrimmedEnv('VITE_PLAN_PRICE_OPERATOR', 'Launch pricing via Paddle'),
ctaLabel: readTrimmedEnv('VITE_PADDLE_CHECKOUT_URL_OPERATOR') ? 'Open Paddle checkout' : 'Request operator access',
ctaHref: readTrimmedEnv('VITE_PADDLE_CHECKOUT_URL_OPERATOR', '/pricing'),
notes: 'Desktop-first recognition, replay, training, and higher-dimensional runtime ownership for active training operators.',
features: [
'Desktop download access',
'Desktop-link token handoff',
'Recognition, replay, and coaching ownership',
'Classic-cube package and validation lane access',
],
},
{
key: 'studio',
name: 'Studio',
price: readTrimmedEnv('VITE_PLAN_PRICE_STUDIO', 'Contact for launch readiness'),
ctaLabel: readTrimmedEnv('VITE_PADDLE_CHECKOUT_URL_STUDIO') ? 'Open Paddle checkout' : 'Talk to HyperTwist',
ctaHref: readTrimmedEnv('VITE_PADDLE_CHECKOUT_URL_STUDIO', '/support'),
notes: 'Higher-dimensional families, operator deployment support, and packaging/validation coordination for production lanes.',
features: [
'Magic120Cell and 5D operator posture',
'Release planning and deployment coordination',
'Desktop distribution and legal-notice readiness',
'Custom support for rollout and training programs',
],
},
] as const
export const downloadTargets = [
{
platform: 'Windows',
subtitle: 'Primary shipping lane',
href: readTrimmedEnv('VITE_WINDOWS_DOWNLOAD_URL'),
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
},
{
platform: 'macOS',
subtitle: 'Planned distribution surface',
href: readTrimmedEnv('VITE_MAC_DOWNLOAD_URL'),
details: 'List a signed desktop build here when the package lane is opened.',
},
{
platform: 'Linux',
subtitle: 'Operator-targeted later lane',
href: readTrimmedEnv('VITE_LINUX_DOWNLOAD_URL'),
details: 'Use for future package publication after the bounded release lane is widened.',
},
] as const
export const publicDocsUrl = readTrimmedEnv('VITE_PUBLIC_DOCS_URL')
export const releaseNotesUrl = readTrimmedEnv('VITE_RELEASE_NOTES_URL')
export const mplSourceUrl = readTrimmedEnv('VITE_MPL_SOURCE_URL')
export const openSourceRepoUrl = readTrimmedEnv('VITE_OPEN_SOURCE_REPO_URL')
export const paddleReadyDescription =
'HyperTwist is a desktop-first training environment for classic cube, recognition-assisted reconstruction, replay explanation, coaching, higher-dimensional runtime ownership, and browser-based operator access. Delivery is digital-only through authenticated account access and desktop downloads. Pricing and checkout are wired for Paddle, while public legal surfaces stay responsible for open-source notices and corresponding-source disclosure when shipped builds contain MPL-covered components.'

153
website/src/site-data.ts Normal file
View file

@ -0,0 +1,153 @@
import { brandConfig, downloadTargets, mplSourceUrl, openSourceRepoUrl, publicDocsUrl, releaseNotesUrl } from './site-config'
export const heroMetrics = [
{ label: 'Current runtime center', value: 'Native Unreal' },
{ label: 'Higher-dimensional ownership', value: '120-cell + 5D' },
{ label: 'Browser posture', value: 'Embedded shell live' },
{ label: 'Package gate', value: 'Phase 10B/10C closed' },
] as const
export const capabilityPillars = [
{
title: 'Recognition to reconstruction',
description: 'Classic-cube intake, correction closure, browser-assisted recognition, and solve guidance are already first-party owned surfaces.',
},
{
title: 'Replay, coaching, and analytics',
description: 'Replay capture, explanation, leaderboard persistence, training analytics, and operator diagnostics are part of the current shipping lane.',
},
{
title: 'Higher-dimensional seriousness',
description: 'Hyper puzzle catalog, replay verification, 120-cell and 5D runtime ownership, and non-Euclidean tiling posture are all represented honestly.',
},
{
title: 'Desktop-first distribution',
description: 'Browser account access supports the operator, while the real simulator and package-validation story remain desktop-first and Unreal-backed.',
},
] as const
export const shippingNowCards = [
'Native Unreal training runtime, coaching cockpit, and generated-mode launch',
'Classic-cube timing, drill flows, local leaderboard persistence, and replay recording',
'Browser-assisted recognition shell with correction closure and recommendation readout',
'Embedded browser runtime with live operator/runtime diagnostics inside Unreal',
'MagicTile browser host bridge plus bounded native behavior proof',
'Higher-dimensional 120-cell and 5D runtime-state, projection, and persistence ownership',
] as const
export const roadmapHonestyCards = [
'The embedded browser shell is shipping. The optional full-browser client remains spec-only and is not presented as a live simulator.',
'Native renderer widening for MagicTile stays explicitly No-Go until a real browser-host gap is evidenced.',
'Pricing and checkout are Paddle-ready, but public launch still requires production checkout URLs and corresponding-source publication for MPL-covered shipped builds.',
'Desktop distribution is the primary product lane. Browser access is for account, operator, release, and support surfaces unless a later browser-client packet is deliberately reopened.',
] as const
export const resourceCollections = [
{
title: 'Product truth',
items: [
'Feature registry-backed descriptions only',
'Roadmap-honest separation between shipped and retained capability',
'Release notes that surface what actually landed',
],
},
{
title: 'Operator rollout',
items: [
'Desktop download instructions and release channels',
'Desktop-link handshake for browser-to-desktop sign-in',
'Legal/notices linkage for public distribution surfaces',
],
},
{
title: 'Training depth',
items: [
'Classic-cube recognition and correction workflows',
'Replay, coaching, analytics, and package validation posture',
'Higher-dimensional puzzle-family references and browser-host boundaries',
],
},
] as const
export const changelogEntries = [
{
date: 'June 22, 2026',
title: 'Public HyperTwist website, auth shell, and distribution surfaces landed',
details: 'A dedicated hypertwist.app app now exists for public pages, browser account access, download posture, public notices, and Paddle-ready pricing.',
},
{
date: 'June 19, 2026',
title: 'Embedded browser runtime hardened for operator diagnostics',
details: 'Runtime-ready, runtime-status, and native training/dashboard diagnostics were typed and surfaced as first-party operator status.',
},
{
date: 'June 19, 2026',
title: 'MagicTile native behavior proof and renderer gate frozen',
details: 'The native behavior proof seam landed and the renderer-port decision remained explicit No-Go without new browser-host failure evidence.',
},
{
date: 'June 18, 2026',
title: 'Classic-cube package/integration validation refreshed',
details: 'Phase 10B and 10C were revalidated with packaged smoke coverage, stricter package helper enforcement, and maintained archive proof.',
},
] as const
export const openSourceNotices = [
{
component: 'cubing/cubing.js',
license: 'MPL-2.0',
whyItMatters: 'HyperTwist relies on an accepted MPL-covered route for current live cubing functionality.',
},
{
component: 'coqui-ai/TTS package code',
license: 'MPL-2.0',
whyItMatters: 'If shipped desktop builds contain this code, public pricing/download pages and the installed app must expose notices and source availability.',
},
{
component: 'Retained permissive browser/runtime sidecars',
license: 'MIT / permissive family',
whyItMatters: 'These support the public site and embedded browser surfaces, but the public legal surface still needs to stay easy to find and stable.',
},
] as const
export const sourceAvailability = {
mplSourceUrl,
openSourceRepoUrl,
docsUrl: publicDocsUrl,
releaseNotesUrl,
}
export const footerLinks = [
{ to: '/about', label: 'About' },
{ to: '/resources', label: 'Resources' },
{ to: '/pricing', label: 'Pricing' },
{ to: '/download', label: 'Download' },
{ to: '/open-source-notices', label: 'Open Source Notices' },
{ to: '/privacy', label: 'Privacy' },
{ to: '/terms', label: 'Terms' },
{ to: '/shipping-payment', label: 'Shipping & Payment' },
] as const
export const supportFaqs = [
{
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.',
},
{
question: 'Can I download a build immediately after sign-in?',
answer: 'Yes, once a release URL is configured for your plan. The dashboard also exposes a desktop-link token so the browser account can pair with the desktop app safely.',
},
{
question: 'Why is there an open-source notices page on pricing and download surfaces?',
answer: 'Because HyperTwist already carries explicit doctrine requiring public legal and corresponding-source linkage whenever a downloadable shipped build contains MPL-covered material.',
},
] as const
export const companyNarrative = {
mission: 'HyperTwist closes the gap between physical cubing practice, deep replay analysis, operator-grade coaching, and serious higher-dimensional puzzle study.',
posture:
'The product is intentionally honest about what already ships, what is retained and planned, and what remains optional or spec-only. That honesty is part of the platform quality bar.',
distribution:
'The website exists to support public positioning, authenticated operator access, pricing, notices, and desktop distribution without pretending the browser itself already replaces the real desktop runtime.',
contactEmail: brandConfig.contact.email,
}

View file

@ -0,0 +1,671 @@
:root {
--ht-bg: #070b18;
--ht-bg-2: #0d1630;
--ht-surface: rgba(13, 20, 41, 0.82);
--ht-surface-strong: rgba(14, 23, 48, 0.94);
--ht-border: rgba(113, 213, 255, 0.18);
--ht-border-strong: rgba(247, 178, 103, 0.34);
--ht-text: #eef4ff;
--ht-muted: #b9c8e0;
--ht-cyan: #54cbff;
--ht-cyan-soft: rgba(84, 203, 255, 0.16);
--ht-amber: #f7b267;
--ht-amber-soft: rgba(247, 178, 103, 0.12);
--ht-success: #9effc7;
--ht-grid: rgba(148, 201, 255, 0.06);
--font-display: "Space Grotesk", "IBM Plex Sans", sans-serif;
--font-body: "IBM Plex Sans", system-ui, sans-serif;
}
html[data-theme="light"] {
--ht-bg: #f4f8ff;
--ht-bg-2: #dbeaff;
--ht-surface: rgba(255, 255, 255, 0.85);
--ht-surface-strong: rgba(255, 255, 255, 0.96);
--ht-border: rgba(9, 54, 122, 0.14);
--ht-border-strong: rgba(194, 104, 15, 0.3);
--ht-text: #081426;
--ht-muted: #4f6686;
--ht-cyan: #0d80b8;
--ht-cyan-soft: rgba(13, 128, 184, 0.12);
--ht-amber: #c8680f;
--ht-amber-soft: rgba(200, 104, 15, 0.12);
--ht-success: #0f8b56;
--ht-grid: rgba(35, 90, 167, 0.08);
}
* {
box-sizing: border-box;
}
html, body, #root {
min-height: 100%;
}
body {
margin: 0;
font-family: var(--font-body);
color: var(--ht-text);
background:
radial-gradient(circle at top, rgba(84, 203, 255, 0.18), transparent 30%),
radial-gradient(circle at 85% 15%, rgba(247, 178, 103, 0.18), transparent 26%),
linear-gradient(180deg, var(--ht-bg), var(--ht-bg-2));
background-attachment: fixed;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(var(--ht-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--ht-grid) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: linear-gradient(180deg, rgba(255,255,255,0.28), transparent 68%);
}
a {
color: inherit;
text-decoration: none;
}
img {
max-width: 100%;
display: block;
}
.site-shell,
.app-shell {
min-height: 100vh;
}
.site-header,
.page-main,
.site-footer,
.app-topbar,
.app-main {
position: relative;
z-index: 1;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 1.25rem clamp(1rem, 3vw, 2.25rem);
border-bottom: 1px solid var(--ht-border);
backdrop-filter: blur(18px);
background: rgba(8, 12, 22, 0.42);
position: sticky;
top: 0;
}
.brand-mark {
display: flex;
align-items: center;
gap: 0.9rem;
min-width: 0;
}
.brand-mark__image,
.app-sidebar__brand-image {
width: 3rem;
height: 3rem;
object-fit: contain;
filter: drop-shadow(0 0 18px rgba(84, 203, 255, 0.2));
}
.brand-mark strong,
.app-sidebar__brand strong {
display: block;
font-family: var(--font-display);
letter-spacing: 0.02em;
}
.brand-mark small,
.app-sidebar__brand p,
.site-footer__brand p,
.app-topbar__user span {
display: block;
color: var(--ht-muted);
}
.site-nav,
.site-header__actions,
.button-row,
.feature-band,
.metric-grid,
.card-grid,
.split-grid,
.panel-grid,
.app-nav,
.site-footer__links {
display: flex;
gap: 1rem;
}
.site-nav {
flex-wrap: wrap;
justify-content: center;
}
.site-nav__link,
.app-nav__link {
padding: 0.7rem 0.95rem;
border-radius: 999px;
color: var(--ht-muted);
transition: 160ms ease;
}
.site-nav__link:hover,
.site-nav__link.is-active,
.app-nav__link:hover,
.app-nav__link.is-active {
color: var(--ht-text);
background: var(--ht-cyan-soft);
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 2.8rem;
padding: 0.8rem 1.1rem;
border-radius: 999px;
border: 1px solid transparent;
font-weight: 700;
cursor: pointer;
transition: transform 160ms ease, background 160ms ease, border-color 160ms ease, color 160ms ease;
}
.button:hover {
transform: translateY(-1px);
}
.button--primary {
background: linear-gradient(120deg, var(--ht-amber), #ffc788);
color: #09111d;
}
.button--ghost {
background: rgba(255, 255, 255, 0.02);
border-color: var(--ht-border);
color: var(--ht-text);
}
.button--full {
width: 100%;
}
.button.is-disabled,
.button:disabled {
opacity: 0.62;
cursor: not-allowed;
transform: none;
}
.page-main {
width: min(1180px, calc(100vw - 2rem));
margin: 0 auto;
padding-bottom: 4rem;
}
.page-hero {
padding: clamp(2.25rem, 6vw, 5rem) 0 1.5rem;
}
.eyebrow {
margin: 0 0 0.65rem;
font-size: 0.79rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--ht-cyan);
font-weight: 700;
}
.page-hero h1,
.app-topbar h1,
.auth-card h1 {
margin: 0;
font-family: var(--font-display);
font-size: clamp(2.4rem, 6vw, 4.8rem);
line-height: 0.98;
}
.page-hero__lede,
.section-heading p,
.hero-copy p,
.card p,
.callout p,
.panel p,
.timeline-entry p,
.auth-card__subtitle {
color: var(--ht-muted);
line-height: 1.7;
}
.hero-panel,
.page-section,
.site-footer,
.auth-card,
.app-sidebar,
.app-topbar,
.panel,
.hero-visual-card,
.metric-card,
.card,
.callout,
.timeline-entry {
border: 1px solid var(--ht-border);
background: var(--ht-surface);
border-radius: 1.5rem;
box-shadow: 0 24px 90px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(18px);
}
.hero-panel {
padding: clamp(1.25rem, 3vw, 2rem);
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.85fr);
gap: 1.5rem;
align-items: center;
}
.hero-copy {
display: grid;
gap: 1.2rem;
}
.hero-visual-card {
padding: 1.2rem;
background:
radial-gradient(circle at top, rgba(84, 203, 255, 0.18), transparent 42%),
linear-gradient(180deg, rgba(15, 24, 48, 0.96), rgba(10, 16, 30, 0.94));
}
.hero-visual-card__image {
width: min(320px, 100%);
margin: 0 auto;
}
.hero-visual-card__caption {
margin: 1rem 0 0;
color: var(--ht-muted);
text-align: center;
}
.metric-grid {
margin-top: 1rem;
flex-wrap: wrap;
}
.metric-card {
flex: 1 1 210px;
padding: 1rem 1.1rem;
}
.metric-card strong {
display: block;
font-family: var(--font-display);
font-size: 1.45rem;
}
.metric-card span {
color: var(--ht-muted);
}
.page-section {
margin-top: 1.4rem;
padding: clamp(1.25rem, 3vw, 1.85rem);
}
.section-heading {
margin-bottom: 1rem;
}
.section-heading h2,
.card h3,
.panel h2,
.timeline-entry h3 {
margin: 0;
font-family: var(--font-display);
}
.card-grid,
.split-grid,
.panel-grid {
flex-wrap: wrap;
}
.card-grid > *,
.split-grid > *,
.panel-grid > * {
flex: 1 1 280px;
}
.card,
.panel,
.callout,
.timeline-entry {
padding: 1.15rem;
}
.card--compact {
display: grid;
gap: 0.8rem;
}
.card--pricing {
display: grid;
gap: 1rem;
}
.top-gap {
margin-top: 1rem;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0 0 0.6rem;
padding: 0.3rem 0.65rem;
border-radius: 999px;
background: var(--ht-amber-soft);
color: var(--ht-amber);
font-size: 0.8rem;
font-weight: 700;
}
.list {
margin: 0;
padding-left: 1.15rem;
display: grid;
gap: 0.6rem;
color: var(--ht-muted);
}
.feature-band {
flex-wrap: wrap;
}
.feature-band__card {
flex: 1 1 280px;
display: block;
padding: 1.2rem;
border: 1px solid var(--ht-border);
border-radius: 1.3rem;
background: rgba(10, 16, 30, 0.56);
}
.feature-band__card--link {
transition: border-color 160ms ease, transform 160ms ease;
}
.feature-band__card--link:hover {
transform: translateY(-2px);
border-color: var(--ht-border-strong);
}
.inline-link {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-top: 0.8rem;
color: var(--ht-cyan);
font-weight: 700;
}
.timeline {
display: grid;
gap: 1rem;
}
.timeline-entry {
border-left: 4px solid var(--ht-amber);
}
.timeline-entry__date {
margin: 0 0 0.4rem;
color: var(--ht-cyan);
font-size: 0.85rem;
font-weight: 700;
}
.site-footer {
width: min(1180px, calc(100vw - 2rem));
margin: 1rem auto 2rem;
padding: 1.2rem;
display: grid;
gap: 0.9rem;
}
.site-footer__links {
flex-wrap: wrap;
}
.site-footer__note {
color: var(--ht-muted);
}
.auth-shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.25rem;
}
.auth-card {
width: min(540px, 100%);
padding: 1.6rem;
}
.auth-form {
display: grid;
gap: 0.7rem;
margin-top: 1rem;
}
.input-label {
font-size: 0.92rem;
color: var(--ht-muted);
}
.input {
width: 100%;
min-height: 2.9rem;
padding: 0.85rem 0.95rem;
border-radius: 1rem;
border: 1px solid var(--ht-border);
background: rgba(7, 12, 24, 0.7);
color: var(--ht-text);
}
.input:focus {
outline: 2px solid rgba(84, 203, 255, 0.24);
outline-offset: 1px;
}
.form-error {
color: #ff9f9f;
margin: 0;
}
.provider-row {
display: grid;
gap: 0.7rem;
margin-top: 1rem;
}
.auth-card__footer {
margin-top: 1rem;
color: var(--ht-muted);
}
.app-shell {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
}
.app-sidebar {
min-height: 100vh;
margin: 0.8rem;
padding: 1rem;
position: sticky;
top: 0.8rem;
align-self: start;
}
.app-sidebar__brand {
display: flex;
align-items: center;
gap: 0.85rem;
margin-bottom: 1.2rem;
}
.app-nav {
flex-direction: column;
}
.app-nav__link {
display: flex;
align-items: center;
gap: 0.7rem;
}
.app-sidebar__footer {
margin-top: 1.25rem;
display: grid;
gap: 0.65rem;
}
.app-main-shell {
padding: 0.8rem 0.8rem 0.8rem 0;
}
.app-topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.2rem;
}
.app-main {
padding-top: 0.9rem;
}
.code-block {
padding: 0.9rem 1rem;
border-radius: 1rem;
background: rgba(3, 9, 20, 0.74);
border: 1px solid var(--ht-border);
overflow-x: auto;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
color: var(--ht-success);
}
.general-page-loader-shell {
position: relative;
min-height: calc(100vh - 10rem);
display: grid;
place-items: center;
overflow: hidden;
padding: 1.5rem;
background: transparent;
}
.general-page-loader-frame {
position: relative;
display: inline-flex;
min-width: min(24rem, 82vw);
flex-direction: column;
align-items: center;
gap: 0.9rem;
border: 1px solid var(--ht-border-strong);
border-radius: 1.2rem;
padding: 1.35rem 1.2rem 1.1rem;
background: rgba(12, 18, 35, 0.72);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
0 0 28px rgba(84, 203, 255, 0.15);
backdrop-filter: blur(14px);
}
.general-page-loader-text {
display: inline-flex;
font-size: 0.81rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ht-muted);
}
.loading-spinner {
color: var(--ht-cyan);
text-shadow:
0 0 6px rgba(84, 203, 255, 0.9),
0 0 18px rgba(84, 203, 255, 0.55),
0 0 32px rgba(84, 203, 255, 0.28);
}
.loading-spinner.is-completing {
color: var(--ht-amber);
text-shadow:
0 0 6px rgba(247, 178, 103, 0.95),
0 0 18px rgba(247, 178, 103, 0.55),
0 0 32px rgba(247, 178, 103, 0.28);
}
@media (max-width: 980px) {
.hero-grid,
.app-shell {
grid-template-columns: 1fr;
}
.site-header,
.app-topbar {
align-items: flex-start;
flex-direction: column;
}
.app-sidebar {
position: static;
}
.app-main-shell {
padding: 0 0.8rem 0.8rem;
}
}
@media (max-width: 720px) {
.site-header,
.page-main,
.site-footer {
width: min(100vw - 1rem, 100%);
}
.site-header {
padding-inline: 0.8rem;
}
.page-main {
padding-bottom: 2rem;
}
.site-nav,
.site-header__actions,
.button-row {
width: 100%;
flex-direction: column;
}
}

View file

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

1
website/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

22
website/tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}

33
website/vite.config.ts Normal file
View file

@ -0,0 +1,33 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('supertokens-auth-react')) {
return 'auth-vendor'
}
if (id.includes('react-router-dom') || id.includes('@tanstack/react-query')) {
return 'app-vendor'
}
if (id.includes('lucide-react')) {
return 'icon-vendor'
}
return undefined
},
},
},
},
server: {
port: 4273,
},
preview: {
port: 4274,
},
test: {
environment: 'jsdom',
},
})