diff --git a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md index dab2811..513211f 100644 --- a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md +++ b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md @@ -134,6 +134,15 @@ That verifier reads frontend plus server env posture, optionally checks live `/api/auth/health`, and fails when the current lane is still missing public download, checkout, cookie-hardening, secret, or source/notices configuration. +The website lane now also separates env scaffolding by posture: + +- `website/.env.example` plus `website/server/.env.example` for local development +- `website/.env.production.example` plus `website/server/.env.production.example` for public-posture scaffolding + +Those production-shaped templates intentionally keep `replace-me` placeholders, +and the readiness command now fails on those placeholder strings so copied +templates cannot be mistaken for real launch config. + This is browser-based user access for the operator/account surface. It is **not** a claim that the simulator itself is now browser-owned. diff --git a/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md b/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md index 111875d..a0e91f5 100644 --- a/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md +++ b/docs/ops/HYPERTWIST_WEBSITE_RUNTIME_CONFIGURATION_GUIDE_2026-06-22.md @@ -45,6 +45,14 @@ Recommended frontend env posture: - real `VITE_MPL_SOURCE_URL` - real `VITE_OPEN_SOURCE_REPO_URL` +The repo now carries two distinct env-template families: + +- `website/.env.example` and `website/server/.env.example` for local development +- `website/.env.production.example` and `website/server/.env.production.example` for public-posture scaffolding + +The production examples intentionally include `replace-me` placeholder values so +the readiness command still fails until real launch values are inserted. + ## Why same-origin is the clean default The current first-party browser lane already supports exact-origin checks, diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index d1d7489..03212ca 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -263,7 +263,7 @@ repo. | Feature | Status | Primary authority | Notes | |---|---|---|---| -| Public `hypertwist.app` marketing shell | Implemented now | first-party `website/` app + feature registry/roadmap authority | HyperTwist now has a dedicated first-party public web surface for homepage, about, resources, pricing, download, support, and legal routes. This lane is separate from the embedded Unreal browser runtime under `Content/Browser/` and does not claim browser-simulator parity. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard. | +| Public `hypertwist.app` marketing shell | Implemented now | first-party `website/` app + feature registry/roadmap authority | HyperTwist now has a dedicated first-party public web surface for homepage, about, resources, pricing, download, support, and legal routes. This lane is separate from the embedded Unreal browser runtime under `Content/Browser/` and does not claim browser-simulator parity. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place. | | Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, billing-secret/map configuration, and local-versus-public runtime deployment posture. | | Desktop download posture and browser-to-desktop pairing | Implemented now | first-party `website/` app + `website/server` desktop-link endpoints | Public download targets, dashboard-side release posture, and short-lived desktop-link token generation/verification are now first-party owned. The current server posture now enforces exact website-origin matching, bounded per-user issuance, one-time token consumption, and billing-backed plan/download entitlement resolution with focused `website/server` tests green on `2026-06-22`, and the verify handshake now returns the same resolved download-entitlement posture the dashboard sees instead of only identity plus plan/role. The public `/download` page now keeps raw download URLs behind the protected dashboard instead of exposing them directly. Actual release URLs remain deployment configuration rather than hardcoded product truth. | | Paddle-ready pricing and billing webhook seam | Implemented now | first-party `website/` app + `website/server` billing endpoint | The public pricing surface now exists with plan structure, checkout-link configuration seams, and the same `/api/billing/paddle/webhook` endpoint family used by the broader product website lane. The current server now verifies `Paddle-Signature` against `PADDLE_WEBHOOK_SECRET` using the documented raw-body HMAC flow, persists a bounded first-party billing state file, and applies verified Paddle events into account/download entitlement state that the browser dashboard consumes, with focused `website/server` tests green on `2026-06-22`. Production checkout URLs, secret management, and broader operator/admin billing workflows remain deployment/application tasks, not shipped-code omissions. | diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index 1ed99a0..4f40653 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -215,7 +215,9 @@ Current consolidated milestone snapshot: login/register surfaces now expose those warnings instead of silently resembling a production-ready lane, and the `website/` package now also ships a first-party external runtime-readiness verifier for env plus live - `/api/auth/health` posture before public launch, + `/api/auth/health` posture before public launch, along with separate local + versus production example env families whose `replace-me` scaffolding is now + explicitly rejected by that verifier, persists a bounded first-party billing-state file, applies verified Paddle events into account/download entitlement state, and surfaces that resolved billing/download posture back through `/api/auth/me`, the protected browser diff --git a/website/.env.production.example b/website/.env.production.example new file mode 100644 index 0000000..9e8d5f0 --- /dev/null +++ b/website/.env.production.example @@ -0,0 +1,21 @@ +VITE_SUPERTOKENS_API_DOMAIN=https://hypertwist.app +VITE_SUPERTOKENS_WEBSITE_DOMAIN=https://hypertwist.app +VITE_SUPERTOKENS_API_BASE_PATH=/auth +VITE_SUPERTOKENS_WEBSITE_BASE_PATH=/auth +VITE_AUTH_API_BASE_URL=https://hypertwist.app +VITE_AUTH_API_TIMEOUT_MS=8000 +VITE_GITHUB_OAUTH_ENABLED=false +VITE_GOOGLE_OAUTH_ENABLED=false +VITE_ORCID_OAUTH_ENABLED=false +VITE_PUBLIC_DOCS_URL=https://hypertwist.app/resources +VITE_RELEASE_NOTES_URL=https://hypertwist.app/changelog +VITE_SUPPORT_EMAIL=hello@hypertwist.app +VITE_WINDOWS_DOWNLOAD_URL=https://downloads.hypertwist.app/replace-me/windows-installer.exe +VITE_MAC_DOWNLOAD_URL= +VITE_LINUX_DOWNLOAD_URL= +VITE_PADDLE_CHECKOUT_URL_OPERATOR=https://buy.paddle.com/replace-me-operator +VITE_PADDLE_CHECKOUT_URL_STUDIO=https://buy.paddle.com/replace-me-studio +VITE_PLAN_PRICE_OPERATOR=Launch pricing via Paddle +VITE_PLAN_PRICE_STUDIO=Contact for launch readiness +VITE_MPL_SOURCE_URL=https://hypertwist.app/open-source/replace-me +VITE_OPEN_SOURCE_REPO_URL=https://git.scriptoriumai.io/scriptoriumadmin/hypertwist diff --git a/website/README.md b/website/README.md index e2ffd3b..abac712 100644 --- a/website/README.md +++ b/website/README.md @@ -39,6 +39,11 @@ Frontend default URL: - `http://localhost:4273` +Environment templates: + +- local development: `website/.env.example` plus `website/server/.env.example` +- public deployment scaffold: `website/.env.production.example` plus `website/server/.env.production.example` + ## Validation ```bash diff --git a/website/scripts/runtime-readiness-lib.mjs b/website/scripts/runtime-readiness-lib.mjs index 444e370..225b197 100644 --- a/website/scripts/runtime-readiness-lib.mjs +++ b/website/scripts/runtime-readiness-lib.mjs @@ -29,6 +29,22 @@ function tryParseUrl(value) { } } +function isPlaceholderLike(value) { + const trimmed = normalizeTrimmed(value).toLowerCase() + if (!trimmed) { + return false + } + + return trimmed.includes('replace-me') + || trimmed.includes('replace_me') + || trimmed.includes('changeme') + || trimmed.includes('change-me') + || trimmed.includes('your-') + || trimmed.includes('your_') + || trimmed.includes('<') + || trimmed.includes('todo') +} + function isLoopbackHostname(hostname) { const normalized = normalizeTrimmed(hostname).toLowerCase() return normalized === 'localhost' @@ -77,6 +93,12 @@ function requireAbsoluteUrl(bucket, label, value) { return parsed } +function rejectPlaceholderValue(bucket, label, value) { + if (isPlaceholderLike(value)) { + pushFailure(bucket, `${label} still contains a placeholder value.`) + } +} + function evaluateFrontendConfig(frontendEnv) { const bucket = createBucket() @@ -88,24 +110,32 @@ function evaluateFrontendConfig(frontendEnv) { // already recorded as failure } else if (!tryParseUrl(frontendEnv.VITE_WINDOWS_DOWNLOAD_URL)) { pushFailure(bucket, 'VITE_WINDOWS_DOWNLOAD_URL must be an absolute URL.') + } else { + rejectPlaceholderValue(bucket, 'VITE_WINDOWS_DOWNLOAD_URL', frontendEnv.VITE_WINDOWS_DOWNLOAD_URL) } if (!requireNonEmpty(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR', frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR)) { // already recorded as failure } else if (!tryParseUrl(frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR)) { pushFailure(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR must be an absolute URL.') + } else { + rejectPlaceholderValue(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR', frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR) } if (!requireNonEmpty(bucket, 'VITE_MPL_SOURCE_URL', frontendEnv.VITE_MPL_SOURCE_URL)) { // already recorded as failure } else if (!tryParseUrl(frontendEnv.VITE_MPL_SOURCE_URL)) { pushFailure(bucket, 'VITE_MPL_SOURCE_URL must be an absolute URL.') + } else { + rejectPlaceholderValue(bucket, 'VITE_MPL_SOURCE_URL', frontendEnv.VITE_MPL_SOURCE_URL) } if (!requireNonEmpty(bucket, 'VITE_OPEN_SOURCE_REPO_URL', frontendEnv.VITE_OPEN_SOURCE_REPO_URL)) { // already recorded as failure } else if (!tryParseUrl(frontendEnv.VITE_OPEN_SOURCE_REPO_URL)) { pushFailure(bucket, 'VITE_OPEN_SOURCE_REPO_URL must be an absolute URL.') + } else { + rejectPlaceholderValue(bucket, 'VITE_OPEN_SOURCE_REPO_URL', frontendEnv.VITE_OPEN_SOURCE_REPO_URL) } for (const [label, url] of [ @@ -139,6 +169,8 @@ function evaluateFrontendConfig(frontendEnv) { if (!normalizeTrimmed(frontendEnv.VITE_PADDLE_CHECKOUT_URL_STUDIO)) { pushWarning(bucket, 'VITE_PADDLE_CHECKOUT_URL_STUDIO is not set; Studio pricing will stay on the contact/support fallback.') + } else { + rejectPlaceholderValue(bucket, 'VITE_PADDLE_CHECKOUT_URL_STUDIO', frontendEnv.VITE_PADDLE_CHECKOUT_URL_STUDIO) } return { @@ -162,6 +194,8 @@ function evaluateServerConfig(serverEnv) { if (!normalizeTrimmed(serverEnv.PADDLE_WEBHOOK_SECRET)) { pushFailure(bucket, 'PADDLE_WEBHOOK_SECRET is missing.') + } else { + rejectPlaceholderValue(bucket, 'PADDLE_WEBHOOK_SECRET', serverEnv.PADDLE_WEBHOOK_SECRET) } for (const [label, url] of [ @@ -209,6 +243,14 @@ function evaluateServerConfig(serverEnv) { pushWarning(bucket, 'Neither PADDLE_PRODUCT_PLAN_MAP nor PADDLE_PRICE_PLAN_MAP is configured; billing plan resolution will rely on webhook custom_data only.') } + if (normalizeTrimmed(serverEnv.PADDLE_PRODUCT_PLAN_MAP)) { + rejectPlaceholderValue(bucket, 'PADDLE_PRODUCT_PLAN_MAP', serverEnv.PADDLE_PRODUCT_PLAN_MAP) + } + + if (normalizeTrimmed(serverEnv.PADDLE_PRICE_PLAN_MAP)) { + rejectPlaceholderValue(bucket, 'PADDLE_PRICE_PLAN_MAP', serverEnv.PADDLE_PRICE_PLAN_MAP) + } + return { ...bucket, apiOrigin: apiDomain?.origin || '', diff --git a/website/scripts/runtime-readiness-lib.test.mjs b/website/scripts/runtime-readiness-lib.test.mjs index dd81380..fbb785d 100644 --- a/website/scripts/runtime-readiness-lib.test.mjs +++ b/website/scripts/runtime-readiness-lib.test.mjs @@ -86,6 +86,36 @@ describe('buildRuntimeReadinessReport', () => { expect(report.failures).toContain('COOKIE_SECURE must be true before public launch.') expect(report.failures).toContain('PADDLE_WEBHOOK_SECRET is missing.') }) + + it('fails placeholder values even when production-shaped env files are otherwise populated', () => { + const report = buildRuntimeReadinessReport({ + frontendEnv: { + VITE_SUPERTOKENS_API_DOMAIN: 'https://hypertwist.app', + VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'https://hypertwist.app', + VITE_AUTH_API_BASE_URL: 'https://hypertwist.app', + VITE_WINDOWS_DOWNLOAD_URL: 'https://downloads.hypertwist.app/replace-me/windows.exe', + VITE_PADDLE_CHECKOUT_URL_OPERATOR: 'https://buy.paddle.com/replace-me-operator', + VITE_PADDLE_CHECKOUT_URL_STUDIO: 'https://buy.paddle.com/replace-me-studio', + VITE_MPL_SOURCE_URL: 'https://hypertwist.app/open-source/replace-me', + VITE_OPEN_SOURCE_REPO_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + }, + serverEnv: { + API_DOMAIN: 'https://hypertwist.app', + WEBSITE_DOMAIN: 'https://hypertwist.app', + SUPERTOKENS_CORE_URI: 'http://127.0.0.1:3567', + COOKIE_SECURE: 'true', + PADDLE_WEBHOOK_SECRET: 'replace-me-paddle-webhook-secret', + PADDLE_PRICE_PLAN_MAP: '{"replace_me_price_operator":"operator"}', + }, + liveHealth: null, + }) + + expect(report.ok).toBe(false) + expect(report.failures).toContain('VITE_WINDOWS_DOWNLOAD_URL still contains a placeholder value.') + expect(report.failures).toContain('VITE_PADDLE_CHECKOUT_URL_OPERATOR still contains a placeholder value.') + expect(report.failures).toContain('PADDLE_WEBHOOK_SECRET still contains a placeholder value.') + expect(report.failures).toContain('PADDLE_PRICE_PLAN_MAP still contains a placeholder value.') + }) }) describe('deriveHealthBaseUrl', () => { diff --git a/website/server/.env.production.example b/website/server/.env.production.example new file mode 100644 index 0000000..58c6449 --- /dev/null +++ b/website/server/.env.production.example @@ -0,0 +1,21 @@ +PORT=3001 +SUPERTOKENS_CORE_URI=http://127.0.0.1:3567 +API_DOMAIN=https://hypertwist.app +WEBSITE_DOMAIN=https://hypertwist.app +API_BASE_PATH=/auth +WEBSITE_BASE_PATH=/auth +COOKIE_SECURE=true +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=replace-me-paddle-webhook-secret +PADDLE_WEBHOOK_TOLERANCE_MS=5000 +BILLING_STATE_PATH=/var/lib/hypertwist/auth/hypertwist-billing-state.json +PADDLE_PRODUCT_PLAN_MAP={"replace_me_product_operator":"operator","replace_me_product_studio":"studio"} +PADDLE_PRICE_PLAN_MAP={"replace_me_price_operator":"operator","replace_me_price_studio":"studio"} diff --git a/website/server/README.md b/website/server/README.md index bf934b1..328bebf 100644 --- a/website/server/README.md +++ b/website/server/README.md @@ -83,3 +83,8 @@ npm run check:runtime-readiness -- --frontend-env .env --server-env server/.env ``` Use `--skip-live-health` when only file-level env verification is possible. + +Keep the example env files separated by posture: + +- `website/.env.example` and `website/server/.env.example` remain local-development defaults +- `website/.env.production.example` and `website/server/.env.production.example` are public-posture scaffolds that still intentionally fail readiness until placeholder values are replaced