From 4c5ceea1000a075ac1ffbcf74eee14963c40ab92 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 24 Jun 2026 00:10:00 +0000 Subject: [PATCH] Centralize launch readiness and expand public manual surfaces --- ...LING_AND_DISTRIBUTION_PACKET_2026-06-22.md | 6 + .../HyperTwist/FEATURE_REGISTRY.md | 2 +- .../src/__tests__/server-bootstrap.test.ts | 25 ++ website/server/src/index.ts | 45 ++- .../__tests__/DashboardOverviewPage.test.tsx | 6 +- website/src/__tests__/public-launch.test.ts | 24 +- .../__tests__/public-marketing-pages.test.tsx | 81 ++++- website/src/auth/auth-api.ts | 9 + .../src/components/ui/PublicLaunchStatus.tsx | 87 +---- website/src/pages/app-pages.tsx | 125 +++---- website/src/pages/public-pages-commerce.tsx | 20 ++ website/src/pages/public-pages-marketing.tsx | 75 ++++ website/src/public-launch.ts | 141 ++++---- website/src/shared/public-launch.ts | 324 ++++++++++++++++++ website/src/site-config.ts | 5 + 15 files changed, 730 insertions(+), 245 deletions(-) create mode 100644 website/src/shared/public-launch.ts diff --git a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md index fec3536..7b1cadd 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 @@ -136,6 +136,12 @@ marketing copy does not imply launch readiness when checkout/download config exists but the live billing webhook or public-origin runtime posture is still incomplete. +That same lane is now additionally server-owned through an authoritative +`launch` summary on `GET /api/auth/health`, carrying blocker labels, billing +product/price-map posture, and current operator/studio checkout targets so the +public banner and protected dashboard no longer recompute launch blockers from +separate client-side sources. + The login and register pages now also surface those shared-auth runtime warnings instead of silently behaving like production auth when the lane is still in local fallback or mixed deployment posture. 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 50cbc6f..d47c611 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -265,7 +265,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 public lane now also serves as a bounded operator/distribution manual through the docs/resources/download/support surfaces, explaining browser-versus-desktop posture, rollout steps, package proof, simulator-use guidance, and current input/device truth without claiming browser ownership of the native runtime or overclaiming unfinished VR/controller posture. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place, bootstrap CI now validates both the frontend and auth-server website commands directly, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin public deployment. Request-level server coverage now also proves that public/app shell delivery does not shadow `/api/*`, `/auth*`, `/health`, or missing asset paths, while the pricing/download/notices routes now surface first-party preview-versus-launch posture from the same bounded launch checklist instead of relying on hidden operator-only status. The shared marketing shell now also carries a compact public-site-status banner across public pages, the homepage keeps a fuller status section, and the shared public launch-status component now consumes live auth-health webhook/runtime truth in addition to release-manifest download readiness so public marketing copy does not claim launch posture from static checkout/download config alone. A later same-family continuation then widened that release-manifest authority again to carry public runtime commerce config for operator/studio checkout URLs and live plan-price strings, so pricing, notices, and dashboard launch-readiness surfaces no longer depend only on frontend build-time checkout config. The live website lane now also owns route-aware title/description/canonical/Open-Graph/Twitter metadata for the real `hypertwist.app` marketing surface so deployed public pages no longer remain on a single generic SPA title/description, plus first-party `robots.txt` and `sitemap.xml` assets for the public route set while keeping `/app`, `/login`, and `/register` out of crawler posture. The real `check-runtime-readiness` CLI is now also exercised against the checked-in production example env files, and a spawned `website/server` bootstrap proof now verifies the live same-origin process path from production-shaped env into `/health`, `/api/auth/health`, built-shell serving, and the public anonymous release-manifest posture for the shared desktop release lane. The same verifier now also probes the deployed root-shell marker and can explicitly fail when the public origin is still serving the older placeholder rollout page instead of the first-party website/auth-server lane, while the repo now also carries first-party `website/deploy/` `nginx` plus `systemd` handoff templates, a concrete same-origin public-host cutover guide, a deployment-file renderer that emits resolved operator outputs from real checkout paths, and a manifest-driven bundle renderer that lets one authoritative input own the public origin while emitting validated env plus install artifacts together, with the shared-VPS-safe default upstream moved to `3011` after live host inspection confirmed `3001` is already occupied by FamiliarOS. The same deployment lane now also distinguishes `launch` from `preview` posture so honest missing checkout/download/webhook/release values are accepted only for non-public rehearsal while placeholder strings still fail, `runtime.mode: mixed` plus `public_origin_ready: true` counts as valid preview-host proof, and the staging helper can archive either committed `HEAD` or the live worktree through `--archive-source worktree`. An isolated VPS-local staging proof then confirmed that both the committed HyperTwist website lane and the later preview-tier worktree packet can serve green health, release-manifest, and first-party shell responses on that real shared host behind `3011`, and a later root-owned cutover then replaced the public placeholder site with the live first-party same-origin preview deployment on `https://hypertwist.app`. The package now also ships a bounded root-owned live-deploy helper that stages the committed checkout, uploads the rendered bundle, installs env, rebuilds the site, replaces the live `systemd` plus `nginx` files, and validates the public origin; that helper has already been re-proved idempotently against the live host. The repo now also ships that host-proof flow as a first-party staging helper so future sessions can rerun the temp checkout/build/boot verification path directly before or after root-owned cutover. The website lane now also owns a sanitized generated packaged-validation summary for the Windows higher-dimensional desktop proof, rendered from the checked-in authoritative package report into `website/src/shared/generated/windows-package-validation-summary.json`, and the owned web-surface validation gate now checks that generated proof for freshness before approving the current public/auth/download/browser lane. | +| 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 public lane now also serves as a bounded operator/distribution manual through the docs/resources/download/support surfaces, explaining browser-versus-desktop posture, rollout steps, package proof, simulator-use guidance, and current input/device truth without claiming browser ownership of the native runtime or overclaiming unfinished VR/controller posture. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place, bootstrap CI now validates both the frontend and auth-server website commands directly, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin public deployment. Request-level server coverage now also proves that public/app shell delivery does not shadow `/api/*`, `/auth*`, `/health`, or missing asset paths, while the pricing/download/notices routes now surface first-party preview-versus-launch posture from the same bounded launch checklist instead of relying on hidden operator-only status. The shared marketing shell now also carries a compact public-site-status banner across public pages, the homepage keeps a fuller status section, and the shared public launch-status component now consumes live auth-health webhook/runtime truth in addition to release-manifest download readiness so public marketing copy does not claim launch posture from static checkout/download config alone. The auth server now also emits an authoritative `launch` summary on `/api/auth/health`, including required blocker labels, billing product/price-map gates, and current checkout targets, so the public banner and protected dashboard no longer duplicate launch-blocker evaluation client-side. A later same-family continuation then widened that release-manifest authority again to carry public runtime commerce config for operator/studio checkout URLs and live plan-price strings, so pricing, notices, and dashboard launch-readiness surfaces no longer depend only on frontend build-time checkout config. The live website lane now also owns route-aware title/description/canonical/Open-Graph/Twitter metadata for the real `hypertwist.app` marketing surface so deployed public pages no longer remain on a single generic SPA title/description, plus first-party `robots.txt` and `sitemap.xml` assets for the public route set while keeping `/app`, `/login`, and `/register` out of crawler posture. The real `check-runtime-readiness` CLI is now also exercised against the checked-in production example env files, and a spawned `website/server` bootstrap proof now verifies the live same-origin process path from production-shaped env into `/health`, `/api/auth/health`, built-shell serving, and the public anonymous release-manifest posture for the shared desktop release lane. The same verifier now also probes the deployed root-shell marker and can explicitly fail when the public origin is still serving the older placeholder rollout page instead of the first-party website/auth-server lane, while the repo now also carries first-party `website/deploy/` `nginx` plus `systemd` handoff templates, a concrete same-origin public-host cutover guide, a deployment-file renderer that emits resolved operator outputs from real checkout paths, and a manifest-driven bundle renderer that lets one authoritative input own the public origin while emitting validated env plus install artifacts together, with the shared-VPS-safe default upstream moved to `3011` after live host inspection confirmed `3001` is already occupied by FamiliarOS. The same deployment lane now also distinguishes `launch` from `preview` posture so honest missing checkout/download/webhook/release values are accepted only for non-public rehearsal while placeholder strings still fail, `runtime.mode: mixed` plus `public_origin_ready: true` counts as valid preview-host proof, and the staging helper can archive either committed `HEAD` or the live worktree through `--archive-source worktree`. An isolated VPS-local staging proof then confirmed that both the committed HyperTwist website lane and the later preview-tier worktree packet can serve green health, release-manifest, and first-party shell responses on that real shared host behind `3011`, and a later root-owned cutover then replaced the public placeholder site with the live first-party same-origin preview deployment on `https://hypertwist.app`. The package now also ships a bounded root-owned live-deploy helper that stages the committed checkout, uploads the rendered bundle, installs env, rebuilds the site, replaces the live `systemd` plus `nginx` files, and validates the public origin; that helper has already been re-proved idempotently against the live host. The repo now also ships that host-proof flow as a first-party staging helper so future sessions can rerun the temp checkout/build/boot verification path directly before or after root-owned cutover. The website lane now also owns a sanitized generated packaged-validation summary for the Windows higher-dimensional desktop proof, rendered from the checked-in authoritative package report into `website/src/shared/generated/windows-package-validation-summary.json`, and the owned web-surface validation gate now checks that generated proof for freshness before approving the current public/auth/download/browser lane. | | 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. The same protected overview now also consumes the server-backed Windows packaged-validation summary that the release-manifest authority exposes, so operators can see current higher-dimensional desktop proof without drilling into the dedicated downloads screen. The adjacent protected `/app/browser-access` and `/app/notices` routes now also reuse the bounded escalation-map and release-follow-through guidance from the public support/manual lane, so sign-in does not degrade operator troubleshooting quality into a thinner shell than the public surface. The protected fallback path now also preserves signed-in viewer plan/access posture from local session truth when live manifest authority is temporarily unavailable, while still keeping download authority conservative until the auth server returns. The shared browser auth layer now also has a bounded release-authority reconciliation path that refreshes and persists the local browser session snapshot from fresher manifest viewer truth, while the dashboard and account surfaces expose a compact live-authority sync notice summarizing what changed so operators can see the session catch-up clearly. That same auth layer now also clears stale stored sessions when shared auth is configured and `/api/auth/me` returns `401`, while still preserving the intended offline/local-fallback posture when the account API is merely unreachable or shared auth is not configured. Focused frontend coverage now also protects deep-link login redirect preservation, safe `next`-path normalization across auth entry points, fallback/email auth-bootstrap normalization, login/register continuation behavior, public download-gating behavior, protected-route/shell behavior, real lazy-route tree behavior for key public and protected paths, top-level app-bootstrap and SuperTokens-wrapper posture, login/register unhappy-path and OAuth-button behavior, support-topic fallback routing when live checkout is not configured, desktop-link verify-url/dashboard readiness behavior, and explicit `noindex,nofollow` posture on protected/auth browser surfaces. The validation lane now also has a bounded signed test-session harness under `TEST_MODE=testing` that proves `/api/auth/me`, stale-session invalidation, release-authority reconciliation, and `/api/auth/desktop-link` behavior through the live spawned auth-server process without widening production auth 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 same lane now also owns a shared `GET /api/releases/manifest` runtime authority for release version/channel/build/published/file-size/checksum/docs/source metadata, with anonymous callers intentionally denied raw download URLs while entitled session-backed callers receive the configured direct platform URL. That manifest now also carries first-party packaged-validation summary truth for the Windows higher-dimensional desktop lane, so the public `/download` page, the public `/resources` reference page, and the protected `/app/downloads` surface can project real package evidence for the dedicated-family `Magic120Cell` / `MagicCube5D` maps even while launch-tier release URLs remain unconfigured. That website-facing packaged proof is now sourced through a sanitized generated summary rendered from the checked-in authoritative `phase6c_dedicated_family_package_validation_report.json`, and the owned web-surface validation gate now checks that generated summary for freshness before approving the current public/auth/download/browser lane. The public `/download` page now keeps raw download URLs behind the protected dashboard instead of exposing them directly, preserves requested platform continuity through `/app/downloads?platform=...`, and surfaces that requested target again after auth handoff inside the protected release lane. When live manifest authority is unavailable, the protected fallback path now still reflects the signed-in viewer posture from local session truth while continuing to withhold raw delivery authority. When live manifest authority resolves fresher entitled viewer truth than the local browser auth snapshot, the protected download-center lane now also follows that server-backed viewer posture for the actual download action instead of leaving the operator blocked behind stale local session state. Both the public and protected download surfaces now also carry first-party rollout steps, first-launch desktop setup guidance, browser-to-desktop pairing follow-through, and release/notices/source references so the desktop setup lane is more than a generic link bucket, while the support surface now carries an explicit escalation map separating account, package, runtime, and rollout/compliance problems. The protected dashboard overview and protected account surface now also mirror that same first-launch follow-through, and the protected browser-access/notices routes now mirror the same escalation separation, so post-sign-in operator guidance does not collapse back into a thinner release-only shell. The dashboard plus public launch-status callouts now consume the same manifest-backed Windows download truth instead of only static frontend config. 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`. The shared `GET /api/releases/manifest` authority now also carries public runtime commerce config for operator/studio checkout URLs and live plan-price strings, allowing the pricing page to switch from frontend build-time checkout assumptions to auth-server runtime truth when those values are configured. A spawned live-process proof now also verifies that a real signed webhook updates processed-event health and persisted billing state through the actual auth-server runtime, not only helper-level store tests, and transaction events no longer leak their id into stored `subscriptionId` state. Production checkout URLs, secret management, and broader operator/admin billing workflows remain deployment/application tasks, not shipped-code omissions. | diff --git a/website/server/src/__tests__/server-bootstrap.test.ts b/website/server/src/__tests__/server-bootstrap.test.ts index 707602e..742c58c 100644 --- a/website/server/src/__tests__/server-bootstrap.test.ts +++ b/website/server/src/__tests__/server-bootstrap.test.ts @@ -305,6 +305,26 @@ describe('website/server bootstrap', { timeout: 15_000 }, () => { warnings: [], errors: [], }, + launch: { + posture: 'launch-ready', + ready: true, + missingLabels: [], + targets: { + operatorCheckoutTarget: 'https://buy.paddle.com/operator-live', + studioCheckoutTarget: 'https://buy.paddle.com/studio-live', + }, + readiness: { + windowsDownloadConfigured: true, + operatorCheckoutConfigured: true, + studioCheckoutConfigured: true, + mplSourceConfigured: true, + openSourceRepoConfigured: true, + billingProductPlanMapConfigured: true, + billingPricePlanMapConfigured: true, + billingWebhookSecretConfigured: true, + publicAuthRuntimeReady: true, + }, + }, }) const releaseManifestResponse = await fetch(`${baseUrl}/api/releases/manifest`) @@ -482,6 +502,11 @@ describe('website/server bootstrap', { timeout: 15_000 }, () => { fallback: { active: false, }, + launch: { + posture: 'launch-ready', + ready: true, + missingLabels: [], + }, }) const stateFile = JSON.parse(readFileSync(billingStatePath, 'utf8')) diff --git a/website/server/src/index.ts b/website/server/src/index.ts index 04d4ac2..cc60580 100644 --- a/website/server/src/index.ts +++ b/website/server/src/index.ts @@ -10,6 +10,7 @@ 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 { resolvePublicLaunchStatusSummary } from '../../src/shared/public-launch' import { probeSuperTokensCoreHealth } from './auth-health' import { createBillingStateStore, type BillingPlan, type BillingRole } from './billing-state' import { verifyPaddleWebhookSignature } from './paddle-webhook' @@ -315,6 +316,31 @@ app.get('/api/auth/health', async (_req, res) => { timeoutMs: SUPERTOKENS_HEALTH_TIMEOUT_MS, }) + const 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), + } + const runtime = { + mode: runtimeConfigDiagnostics.mode, + public_origin_ready: runtimeConfigDiagnostics.publicOriginReady, + cookie_secure: runtimeConfigDiagnostics.cookieSecure, + api_domain: runtimeConfigDiagnostics.apiDomain, + website_domain: runtimeConfigDiagnostics.websiteDomain, + warnings: runtimeConfigDiagnostics.warnings, + errors: runtimeConfigDiagnostics.errors, + } + const publicManifest = createReleaseManifestResponse(createAnonymousReleaseManifestViewer()).manifest + const launch = resolvePublicLaunchStatusSummary({ + manifest: publicManifest, + health: { + billing, + runtime, + }, + }) + res.json({ ok: true, service: 'hypertwist-auth-server', @@ -334,22 +360,9 @@ app.get('/api/auth/health', async (_req, res) => { 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), - }, - runtime: { - mode: runtimeConfigDiagnostics.mode, - public_origin_ready: runtimeConfigDiagnostics.publicOriginReady, - cookie_secure: runtimeConfigDiagnostics.cookieSecure, - api_domain: runtimeConfigDiagnostics.apiDomain, - website_domain: runtimeConfigDiagnostics.websiteDomain, - warnings: runtimeConfigDiagnostics.warnings, - errors: runtimeConfigDiagnostics.errors, - }, + billing, + runtime, + launch, }) }) diff --git a/website/src/__tests__/DashboardOverviewPage.test.tsx b/website/src/__tests__/DashboardOverviewPage.test.tsx index 38219f4..1124962 100644 --- a/website/src/__tests__/DashboardOverviewPage.test.tsx +++ b/website/src/__tests__/DashboardOverviewPage.test.tsx @@ -181,8 +181,10 @@ describe('DashboardOverviewPage', () => { expect(screen.getByText(/This session is currently using local fallback posture/i)).toBeTruthy() expect(screen.getByText(/Shared auth core is not fully ready right now/i)).toBeTruthy() expect(screen.getByText(/Public launch is not fully configured yet:/i)).toBeTruthy() - expect(screen.getByText(/Paddle webhook secret missing/i)).toBeTruthy() - expect(screen.getByText(/Public auth runtime still uses local or mixed deployment posture/i)).toBeTruthy() + expect(screen.getByText('Billing product-plan map: missing')).toBeTruthy() + expect(screen.getByText('Billing price-plan map: missing')).toBeTruthy() + expect(screen.getByText('Paddle webhook secret: missing')).toBeTruthy() + expect(screen.getByText('Public auth runtime posture: local-or-mixed')).toBeTruthy() expect(screen.getByText('Packaged validation passed')).toBeTruthy() expect(screen.getByText(/Magic120Cell dedicated-family training map: passed/i)).toBeTruthy() expect(screen.getByText('Desktop rollout follow-through')).toBeTruthy() diff --git a/website/src/__tests__/public-launch.test.ts b/website/src/__tests__/public-launch.test.ts index 1d1430d..8e16fc8 100644 --- a/website/src/__tests__/public-launch.test.ts +++ b/website/src/__tests__/public-launch.test.ts @@ -13,12 +13,18 @@ describe('public launch readiness helpers', () => { studioCheckoutConfigured: false, mplSourceConfigured: false, openSourceRepoConfigured: true, + }, { + requiredOnly: true, }) expect(missingItems.map((item) => item.label)).toEqual([ - 'Operator checkout', - 'Studio checkout', - 'Corresponding-source URL', + 'Operator checkout URL', + 'Studio checkout URL', + 'MPL corresponding-source URL', + 'Billing product-plan map', + 'Billing price-plan map', + 'Paddle webhook secret', + 'Public auth runtime posture', ]) expect(isPublicLaunchReady({ windowsDownloadConfigured: true, @@ -32,19 +38,31 @@ describe('public launch readiness helpers', () => { it('treats the bounded website lane as launch-ready only when the full checklist is configured', () => { const checklist = getPublicLaunchChecklist({ windowsDownloadConfigured: true, + macosDownloadConfigured: true, + linuxDownloadConfigured: true, operatorCheckoutConfigured: true, studioCheckoutConfigured: true, mplSourceConfigured: true, openSourceRepoConfigured: true, + billingProductPlanMapConfigured: true, + billingPricePlanMapConfigured: true, + billingWebhookSecretConfigured: true, + publicAuthRuntimeReady: true, }) expect(checklist.every((item) => item.configured)).toBe(true) expect(isPublicLaunchReady({ windowsDownloadConfigured: true, + macosDownloadConfigured: false, + linuxDownloadConfigured: false, operatorCheckoutConfigured: true, studioCheckoutConfigured: true, mplSourceConfigured: true, openSourceRepoConfigured: true, + billingProductPlanMapConfigured: true, + billingPricePlanMapConfigured: true, + billingWebhookSecretConfigured: true, + publicAuthRuntimeReady: true, })).toBe(true) }) }) diff --git a/website/src/__tests__/public-marketing-pages.test.tsx b/website/src/__tests__/public-marketing-pages.test.tsx index 31ff0f4..023e120 100644 --- a/website/src/__tests__/public-marketing-pages.test.tsx +++ b/website/src/__tests__/public-marketing-pages.test.tsx @@ -102,6 +102,7 @@ vi.mock('../site-config', () => ({ })) import { + AboutPage, DocsPage, DownloadPage, HomeLanding, @@ -236,7 +237,7 @@ describe('public marketing pages', () => { expect(screen.getByRole('link', { name: /sign in for windows access/i }).getAttribute('href')).toBe('/app/downloads?platform=windows') expect(screen.getAllByText('Preview posture').length).toBeGreaterThan(0) - expect(screen.getByText('Operator checkout: missing')).toBeTruthy() + expect(screen.getByText('Operator checkout URL: missing')).toBeTruthy() expect(screen.getByText('How the release lane works')).toBeTruthy() expect(screen.getByText('First launch and desktop setup')).toBeTruthy() expect(screen.getByText('Pair desktop access to the browser account')).toBeTruthy() @@ -347,12 +348,83 @@ describe('public marketing pages', () => { expect(screen.getByText('Current public site status')).toBeTruthy() expect(await screen.findByText('Public website and release posture')).toBeTruthy() expect(screen.getAllByText('Preview posture').length).toBeGreaterThan(0) - expect(screen.getByText('Paddle webhook secret: missing')).toBeTruthy() + expect(screen.getAllByText('Paddle webhook secret: missing').length).toBeGreaterThan(0) expect(screen.getByText('Public auth runtime posture: production-ready')).toBeTruthy() + expect(screen.getByText('How a real first session flows')).toBeTruthy() expect(screen.getByText('Why both browser and desktop stay')).toBeTruthy() expect(screen.getAllByText('Native Unreal desktop runtime').length).toBeGreaterThan(0) }) + it('surfaces about-page workflow and current XR truth without pretending the web replaces the simulator', () => { + mockGetAuthHealth.mockResolvedValue({ + ok: true, + service: 'hypertwist-auth-server', + supertokens: { + configured: true, + reachable: true, + ready: true, + apiVersion: '5.4', + error: null, + oauth: { + github: false, + google: false, + }, + }, + fallback: { + enabled: true, + active: false, + reason: null, + }, + billing: { + statePath: '/var/lib/hypertwist/auth/hypertwist-billing-state.json', + processedEventCount: 0, + pricePlanMapConfigured: true, + productPlanMapConfigured: true, + webhookSecretConfigured: true, + }, + runtime: { + mode: 'public', + public_origin_ready: true, + cookie_secure: true, + api_domain: 'https://hypertwist.app', + website_domain: 'https://hypertwist.app', + warnings: [], + errors: [], + }, + }) + mockGetReleaseManifest.mockResolvedValue({ + ok: true, + manifest: { + generated_at: '2026-06-22T12:00:00.000Z', + support_email: 'hello@hypertwist.app', + public_docs_url: 'https://docs.hypertwist.app', + release_notes_url: 'https://notes.hypertwist.app', + corresponding_source_url: 'https://hypertwist.app/open-source/source.zip', + open_source_repo_url: 'https://github.com/hypertwist/hypertwist', + commerce: { + operator_checkout_url: 'https://buy.paddle.com/operator-live', + studio_checkout_url: 'https://buy.paddle.com/studio-live', + plan_price_operator: '$19 / month', + plan_price_studio: '$99 / month', + }, + viewer: { + authenticated: false, + canDownload: false, + plan: null, + role: null, + accessStatus: null, + }, + platforms: [], + }, + }) + + renderWithProviders(, ['/about']) + + expect(screen.getByText('How a real HyperTwist session unfolds')).toBeTruthy() + expect(screen.getByText('Current desktop control and XR truth')).toBeTruthy() + expect(screen.getByText('Release and deployment maturity')).toBeTruthy() + }) + it('uses useful support fallback routes for plans that do not yet have live checkouts', () => { mockGetAuthHealth.mockResolvedValue({ ok: true, @@ -482,6 +554,7 @@ describe('public marketing pages', () => { expect(await screen.findByText('$19 / month')).toBeTruthy() expect(screen.getByText('$99 / month')).toBeTruthy() + expect(screen.getAllByText('What happens after access is granted').length).toBeGreaterThan(0) const checkoutLinks = screen.getAllByRole('link', { name: /open paddle checkout/i }) expect(checkoutLinks).toHaveLength(2) expect(checkoutLinks[0].getAttribute('href')).toBe('https://buy.paddle.com/operator-live') @@ -664,7 +737,7 @@ describe('public marketing pages', () => { expect(screen.getByText('Higher-dimensional runtime guide')).toBeTruthy() expect(screen.getByText('Current control and device posture')).toBeTruthy() expect(screen.getByText('Deployment readiness snapshot')).toBeTruthy() - expect(screen.getByText('XR groundwork exists, but the full VR lane is not finished')).toBeTruthy() + expect(screen.getAllByText('XR groundwork exists, but the full VR lane is not finished').length).toBeGreaterThan(0) expect(screen.getByText('Higher-dimensional runtime ownership')).toBeTruthy() }) @@ -728,7 +801,7 @@ describe('public marketing pages', () => { renderWithProviders(, ['/docs']) expect(screen.getByText('Operator manual')).toBeTruthy() - expect(screen.getByText('1. Start in the browser shell')).toBeTruthy() + expect(screen.getAllByText('1. Start in the browser shell').length).toBeGreaterThan(0) expect(screen.getByText('Simulator manual')).toBeTruthy() expect(screen.getByText('Higher-dimensional family guide')).toBeTruthy() expect(screen.getByText('Deployment readiness manual')).toBeTruthy() diff --git a/website/src/auth/auth-api.ts b/website/src/auth/auth-api.ts index b8e2fd4..c8b1774 100644 --- a/website/src/auth/auth-api.ts +++ b/website/src/auth/auth-api.ts @@ -1,4 +1,5 @@ import { getAuthRuntimeConfig } from './auth-env' +import type { PublicLaunchChecklistItem, PublicLaunchReadiness, PublicLaunchTargets } from '../shared/public-launch' const authRuntime = getAuthRuntimeConfig() const DEFAULT_AUTH_API_TIMEOUT_MS = authRuntime.authApiTimeoutMs @@ -90,6 +91,14 @@ export interface AuthHealthPayload { warnings: string[] errors: string[] } + launch?: { + posture: 'launch-ready' | 'preview' + ready: boolean + readiness: PublicLaunchReadiness + checklist: PublicLaunchChecklistItem[] + missingLabels: string[] + targets: PublicLaunchTargets + } } export interface ReleaseManifestPlatformPayload { diff --git a/website/src/components/ui/PublicLaunchStatus.tsx b/website/src/components/ui/PublicLaunchStatus.tsx index 3949864..8b805fc 100644 --- a/website/src/components/ui/PublicLaunchStatus.tsx +++ b/website/src/components/ui/PublicLaunchStatus.tsx @@ -1,14 +1,8 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Link } from 'react-router-dom' -import { getAuthHealth, getReleaseManifest } from '../../auth/auth-api' -import { - getMissingPublicLaunchChecklistItems, - getPublicLaunchChecklist, - isPublicLaunchReady, - resolvePublicLaunchReadiness, - type PublicLaunchReadiness, -} from '../../public-launch' +import { getAuthHealth } from '../../auth/auth-api' +import { resolvePublicLaunchStatusSummary } from '../../public-launch' import { launchReadiness } from '../../site-config' import { buildSupportPath } from '../../site-routes' @@ -18,48 +12,29 @@ function usePublicLaunchStatus() { queryFn: getAuthHealth, retry: false, }) - const releaseManifestQuery = useQuery({ - queryKey: ['release-manifest', 'public'], - queryFn: getReleaseManifest, - retry: false, - }) + const launchStatus = useMemo(() => ( + resolvePublicLaunchStatusSummary(authHealthQuery.data, undefined, launchReadiness) + ), [authHealthQuery.data]) - const readiness = useMemo(() => { - return resolvePublicLaunchReadiness(releaseManifestQuery.data?.manifest, launchReadiness) - }, [releaseManifestQuery.data?.manifest]) - - const checklist = getPublicLaunchChecklist(readiness) - const missingItems = getMissingPublicLaunchChecklistItems(readiness) - const webhookReady = authHealthQuery.data?.billing.webhookSecretConfigured === true - const runtimeReady = authHealthQuery.data?.runtime.public_origin_ready === true - const ready = isPublicLaunchReady(readiness) && webhookReady && runtimeReady + const checklist = launchStatus.checklist + const ready = launchStatus.ready const missingLabels = useMemo(() => { - const labels = missingItems.map((item) => item.label.toLowerCase()) + const labels = launchStatus.missingLabels.map((label) => label.toLowerCase()) if (authHealthQuery.isLoading || authHealthQuery.isError) { labels.push('live auth runtime verification') - return labels - } - - if (!webhookReady) { - labels.push('billing webhook secret') - } - - if (!runtimeReady) { - labels.push('public auth runtime posture') + labels.push('release-manifest authority confirmation') } return labels - }, [authHealthQuery.isError, authHealthQuery.isLoading, missingItems, runtimeReady, webhookReady]) + }, [authHealthQuery.isError, authHealthQuery.isLoading, launchStatus.missingLabels]) return { authHealthQuery, checklist, missingLabels, ready, - releaseManifestQuery, - runtimeReady, - webhookReady, + launchStatus, } } @@ -96,7 +71,7 @@ export function PublicLaunchStatus({ }: { title?: string }) { - const { authHealthQuery, checklist, missingLabels, ready, releaseManifestQuery, runtimeReady, webhookReady } = usePublicLaunchStatus() + const { authHealthQuery, checklist, launchStatus, missingLabels, ready } = usePublicLaunchStatus() return (
@@ -112,44 +87,20 @@ export function PublicLaunchStatus({
    {checklist.map((item) => (
  • - {item.label}: {item.configured ? 'configured' : 'missing'} + {item.label}: {item.configured + ? (item.id === 'public-auth-runtime' ? 'production-ready' : 'configured') + : (item.id === 'public-auth-runtime' ? 'local-or-mixed' : 'missing')}
  • ))} -
  • - Paddle webhook secret:{' '} - {authHealthQuery.isLoading - ? 'checking' - : authHealthQuery.isError - ? 'unknown' - : webhookReady - ? 'configured' - : 'missing'} -
  • -
  • - Public auth runtime posture:{' '} - {authHealthQuery.isLoading - ? 'checking' - : authHealthQuery.isError - ? 'unknown' - : runtimeReady - ? 'production-ready' - : 'local-or-mixed'} -
  • +
  • Operator checkout target: {launchStatus.targets.operatorCheckoutTarget || 'support fallback active'}
  • +
  • Studio checkout target: {launchStatus.targets.studioCheckoutTarget || 'support fallback active'}
{authHealthQuery.isLoading ? ( -

Checking live auth-runtime and billing-webhook posture from the deployed auth server.

+

Checking live launch-readiness posture from the deployed auth server.

) : null} {authHealthQuery.isError ? (

- Live auth-health lookup failed. Launch posture cannot confirm runtime or billing-webhook readiness right now. -

- ) : null} - {releaseManifestQuery.isLoading ? ( -

Refreshing Windows release readiness from the live auth-server manifest.

- ) : null} - {releaseManifestQuery.isError ? ( -

- Live release-manifest lookup failed. This launch checklist is currently using a bounded frontend fallback and intentionally withholds direct download-lane authority until the auth server returns. + Live auth-health lookup failed. Launch posture cannot confirm runtime, billing, or release-authority readiness right now.

) : null} {!ready ? ( diff --git a/website/src/pages/app-pages.tsx b/website/src/pages/app-pages.tsx index 868b4d9..9eeb1f6 100644 --- a/website/src/pages/app-pages.tsx +++ b/website/src/pages/app-pages.tsx @@ -5,9 +5,9 @@ import { buildAuthApiBaseUrls, createDesktopLinkToken, getAuthHealth, getRelease import { usePlatformAuth, type PlatformUser } from '../auth/platform-auth' import { SiteMetadata } from '../components/seo/SiteMetadata' import { ReleaseValidationSummary } from '../components/ui/ReleaseValidationSummary' -import { resolvePublicLaunchReadiness } from '../public-launch' -import { downloadTargets, launchReadiness, mplSourceUrl, openSourceRepoUrl, planCatalog, publicDocsUrl, releaseNotesUrl } from '../site-config' -import { buildReleaseMetadataItems, resolveReleaseCommerceView, resolveReleaseManifestView, type ReleaseManifestView } from '../release-manifest' +import { resolvePublicLaunchStatusSummary } from '../public-launch' +import { downloadTargets, launchReadiness, mplSourceUrl, openSourceRepoUrl, publicDocsUrl, releaseNotesUrl } from '../site-config' +import { buildReleaseMetadataItems, resolveReleaseManifestView, type ReleaseManifestView } from '../release-manifest' import { buildSupportPath, getDownloadPlatformLabel, normalizeDownloadPlatform } from '../site-routes' import { desktopDownloadSteps, desktopFirstLaunchCards, roadmapHonestyCards, supportEscalationCards } from '../site-data' @@ -29,15 +29,6 @@ function Panel({ ) } -const operatorFallbackPlan = planCatalog.find((plan) => plan.key === 'operator') ?? planCatalog[1] -const studioFallbackPlan = planCatalog.find((plan) => plan.key === 'studio') ?? planCatalog[2] -const releaseCommerceFallback = { - operatorCheckoutUrl: /^https?:/i.test(operatorFallbackPlan.ctaHref) ? operatorFallbackPlan.ctaHref : '', - studioCheckoutUrl: /^https?:/i.test(studioFallbackPlan.ctaHref) ? studioFallbackPlan.ctaHref : '', - planPriceOperator: operatorFallbackPlan.price, - planPriceStudio: studioFallbackPlan.price, -} - function buildProtectedReleaseManifestFallback(user: PlatformUser | null | undefined, supportEmail: string) { return { downloadTargets, @@ -283,27 +274,14 @@ export function DashboardOverviewPage() { }, }) - const releaseCommerce = useMemo( - () => resolveReleaseCommerceView(releaseManifestQuery.data?.manifest, releaseCommerceFallback), - [releaseManifestQuery.data?.manifest], - ) - const publicLaunchReadiness = useMemo( - () => resolvePublicLaunchReadiness(releaseManifestQuery.data?.manifest, launchReadiness), - [releaseManifestQuery.data?.manifest], - ) + const launchStatus = useMemo(() => ( + resolvePublicLaunchStatusSummary( + healthQuery.data, + releaseManifestQuery.data?.manifest, + launchReadiness, + ) + ), [healthQuery.data, releaseManifestQuery.data?.manifest]) - const manifestWindowsConfigured = useMemo( - () => releaseManifest.platforms.some((platform) => platform.platform_key === 'windows' && platform.configured), - [releaseManifest.platforms], - ) - const manifestMacConfigured = useMemo( - () => releaseManifest.platforms.some((platform) => platform.platform_key === 'macos' && platform.configured), - [releaseManifest.platforms], - ) - const manifestLinuxConfigured = useMemo( - () => releaseManifest.platforms.some((platform) => platform.platform_key === 'linux' && platform.configured), - [releaseManifest.platforms], - ) const manifestWindowsTarget = useMemo( () => releaseManifest.platforms.find((platform) => platform.platform_key === 'windows') || null, [releaseManifest.platforms], @@ -317,17 +295,7 @@ export function DashboardOverviewPage() { return `${baseUrl}/api/auth/desktop-link/verify?token=${encodeURIComponent(token)}` }, [desktopLinkMutation.data?.token]) - const launchReadinessIssues = useMemo(() => { - const issues: string[] = [] - if (!manifestWindowsConfigured) issues.push('Windows download URL missing') - if (!publicLaunchReadiness.operatorCheckoutConfigured) issues.push('Operator checkout URL missing') - if (!publicLaunchReadiness.studioCheckoutConfigured) issues.push('Studio checkout URL missing') - if (!publicLaunchReadiness.mplSourceConfigured) issues.push('MPL corresponding-source URL missing') - if (!publicLaunchReadiness.openSourceRepoConfigured) issues.push('Open-source repository/notices URL missing') - if (healthQuery.data && !healthQuery.data.billing.webhookSecretConfigured) issues.push('Paddle webhook secret missing') - if (healthQuery.data && !healthQuery.data.runtime.public_origin_ready) issues.push('Public auth runtime still uses local or mixed deployment posture') - return issues - }, [healthQuery.data, manifestWindowsConfigured, publicLaunchReadiness]) + const launchReadinessIssues = launchStatus.missingLabels return ( <> @@ -339,30 +307,30 @@ export function DashboardOverviewPage() { />
-

{user?.name}

-

{user?.email}

-

Plan: {user?.plan}

-

Role: {user?.role || 'operator'}

-

Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}

- {!superTokensConfigured || user?.billing?.source === 'local-fallback' ? ( -

- This session is currently using local fallback posture, not fully shared production auth. -

- ) : null} - {releaseManifestQuery.data?.manifest && releaseAuthoritySyncItems.length > 0 ? ( -
-

Live authority sync

-

- The protected browser session was refreshed from live release authority so the local dashboard view catches up to current account-access truth. +

{user?.name}

+

{user?.email}

+

Plan: {user?.plan}

+

Role: {user?.role || 'operator'}

+

Desktop downloads: {user?.canDownload ? 'enabled' : 'not yet entitled'}

+ {!superTokensConfigured || user?.billing?.source === 'local-fallback' ? ( +

+ This session is currently using local fallback posture, not fully shared production auth.

-
    - {releaseAuthoritySyncItems.map((item) => ( -
  • {item}
  • - ))} -
-
- ) : null} -
+ ) : null} + {releaseManifestQuery.data?.manifest && releaseAuthoritySyncItems.length > 0 ? ( +
+

Live authority sync

+

+ The protected browser session was refreshed from live release authority so the local dashboard view catches up to current account-access truth. +

+
    + {releaseAuthoritySyncItems.map((item) => ( +
  • {item}
  • + ))} +
+
+ ) : null} + {healthQuery.isLoading ?

Checking auth server health...

: null} @@ -440,20 +408,19 @@ export function DashboardOverviewPage() {
+

+ {launchStatus.ready ? 'Launch-ready posture' : 'Preview posture'} +

    -
  • Windows download URL: {manifestWindowsConfigured ? 'configured' : 'missing'}
  • -
  • macOS download URL: {manifestMacConfigured ? 'configured' : 'missing'}
  • -
  • Linux download URL: {manifestLinuxConfigured ? 'configured' : 'missing'}
  • -
  • Operator checkout URL: {publicLaunchReadiness.operatorCheckoutConfigured ? 'configured' : 'missing'}
  • -
  • Studio checkout URL: {publicLaunchReadiness.studioCheckoutConfigured ? 'configured' : 'missing'}
  • -
  • MPL corresponding-source URL: {publicLaunchReadiness.mplSourceConfigured ? 'configured' : 'missing'}
  • -
  • Open-source repo/notices URL: {publicLaunchReadiness.openSourceRepoConfigured ? 'configured' : 'missing'}
  • -
  • Paddle webhook secret: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.webhookSecretConfigured ? 'configured' : 'missing')}
  • -
  • Billing product map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.productPlanMapConfigured ? 'configured' : 'missing')}
  • -
  • Billing price map: {!healthQuery.data ? 'checking' : (healthQuery.data.billing.pricePlanMapConfigured ? 'configured' : 'missing')}
  • -
  • Public auth runtime posture: {!healthQuery.data ? 'checking' : (healthQuery.data.runtime.public_origin_ready ? 'production-ready' : 'local-or-mixed')}
  • -
  • Operator checkout target: {releaseCommerce.operator_checkout_url || 'support fallback active'}
  • -
  • Studio checkout target: {releaseCommerce.studio_checkout_url || 'support fallback active'}
  • + {launchStatus.checklist.map((item) => ( +
  • + {item.label}: {item.configured + ? (item.id === 'public-auth-runtime' ? 'production-ready' : 'configured') + : (item.id === 'public-auth-runtime' ? 'local-or-mixed' : 'missing')} +
  • + ))} +
  • Operator checkout target: {launchStatus.targets.operatorCheckoutTarget || 'support fallback active'}
  • +
  • Studio checkout target: {launchStatus.targets.studioCheckoutTarget || 'support fallback active'}
{releaseManifestQuery.isLoading ? (

Refreshing release-manifest download readiness from the auth server.

diff --git a/website/src/pages/public-pages-commerce.tsx b/website/src/pages/public-pages-commerce.tsx index 83c4a0e..d6b3379 100644 --- a/website/src/pages/public-pages-commerce.tsx +++ b/website/src/pages/public-pages-commerce.tsx @@ -21,6 +21,7 @@ import { digitalDeliveryCards, distributionDoctrineCards, openSourceNotices, + operatorManualTracks, privacyBoundaryCards, termsBoundaryCards, } from '../site-data' @@ -94,6 +95,25 @@ export function PricingPage() { +
+
+ {operatorManualTracks.slice(0, 3).map((track) => ( +
+

{track.title}

+

{track.description}

+
    + {track.steps.map((step) => ( +
  • {step}
  • + ))} +
+
+ ))} +
+
+
+
+
+ {operatorManualTracks.slice(0, 3).map((track) => ( +
+

{track.title}

+

{track.description}

+
    + {track.steps.map((step) => ( +
  • {step}
  • + ))} +
+
+ ))} +
+
+
+ +
+
+ {operatorManualTracks.map((track) => ( +
+

{track.title}

+

{track.description}

+
    + {track.steps.map((step) => ( +
  • {step}
  • + ))} +
+
+ ))} +
+
+ +
+
+ {inputAndDevicePostureCards.map((card) => ( +
+

{card.title}

+

{card.description}

+
    + {card.bullets.map((bullet) => ( +
  • {bullet}
  • + ))} +
+
+ ))} +
+
+ +
+
+ {deploymentReadinessTracks.map((track) => ( +
+

{track.title}

+
    + {track.steps.map((step) => ( +
  • {step}
  • + ))} +
+
+ ))} +
+
) diff --git a/website/src/public-launch.ts b/website/src/public-launch.ts index 0736b59..1de9055 100644 --- a/website/src/public-launch.ts +++ b/website/src/public-launch.ts @@ -1,92 +1,89 @@ +import type { AuthHealthPayload } from './auth/auth-api' import type { ReleaseManifestView } from './release-manifest' import { launchReadiness } from './site-config' +import { + buildPublicLaunchStatusSummary, + getMissingPublicLaunchChecklistItems as getSharedMissingPublicLaunchChecklistItems, + getPublicLaunchChecklist as getSharedPublicLaunchChecklist, + isPublicLaunchReady as isSharedPublicLaunchReady, + normalizePublicLaunchReadiness, + resolveHealthPublicLaunchReadiness, + resolveManifestPublicLaunchReadiness, + resolvePublicLaunchStatusSummary as resolveSharedPublicLaunchStatusSummary, + type PublicLaunchChecklistId, + type PublicLaunchChecklistItem, + type PublicLaunchReadiness, + type PublicLaunchStatusSummary, + type PublicLaunchTargets, +} from './shared/public-launch' -type PublicLaunchChecklistId = - | 'windows-download' - | 'operator-checkout' - | 'studio-checkout' - | 'mpl-source' - | 'open-source-notices' +const defaultLaunchReadiness = normalizePublicLaunchReadiness(launchReadiness) -export type PublicLaunchChecklistItem = { - id: PublicLaunchChecklistId - label: string - configured: boolean -} - -export type PublicLaunchReadiness = { - windowsDownloadConfigured: boolean - operatorCheckoutConfigured: boolean - studioCheckoutConfigured: boolean - mplSourceConfigured: boolean - openSourceRepoConfigured: boolean +export type { + PublicLaunchChecklistId, + PublicLaunchChecklistItem, + PublicLaunchReadiness, + PublicLaunchStatusSummary, + PublicLaunchTargets, } export function getPublicLaunchChecklist( - readiness: PublicLaunchReadiness = launchReadiness, + readiness: Partial = defaultLaunchReadiness, ): PublicLaunchChecklistItem[] { - return [ - { - id: 'windows-download', - label: 'Windows download URL', - configured: readiness.windowsDownloadConfigured, - }, - { - id: 'operator-checkout', - label: 'Operator checkout', - configured: readiness.operatorCheckoutConfigured, - }, - { - id: 'studio-checkout', - label: 'Studio checkout', - configured: readiness.studioCheckoutConfigured, - }, - { - id: 'mpl-source', - label: 'Corresponding-source URL', - configured: readiness.mplSourceConfigured, - }, - { - id: 'open-source-notices', - label: 'Open-source notices/repository URL', - configured: readiness.openSourceRepoConfigured, - }, - ] + return getSharedPublicLaunchChecklist(readiness) } export function getMissingPublicLaunchChecklistItems( - readiness: PublicLaunchReadiness = launchReadiness, + readiness: Partial = defaultLaunchReadiness, + options?: { + requiredOnly?: boolean + }, ): PublicLaunchChecklistItem[] { - return getPublicLaunchChecklist(readiness).filter((item) => !item.configured) + return getSharedMissingPublicLaunchChecklistItems(readiness, options) } -export function isPublicLaunchReady(readiness: PublicLaunchReadiness = launchReadiness): boolean { - return getMissingPublicLaunchChecklistItems(readiness).length === 0 +export function isPublicLaunchReady( + readiness: Partial = defaultLaunchReadiness, +): boolean { + return isSharedPublicLaunchReady(readiness) } export function resolvePublicLaunchReadiness( manifest: ReleaseManifestView | null | undefined, - fallback: PublicLaunchReadiness = launchReadiness, + fallback: Partial = defaultLaunchReadiness, ): PublicLaunchReadiness { - return { - windowsDownloadConfigured: manifest?.platforms.some( - (platform) => platform.platform_key === 'windows' && platform.configured, - ) ?? fallback.windowsDownloadConfigured, - operatorCheckoutConfigured: - manifest?.commerce?.operator_checkout_url != null - ? Boolean(manifest.commerce.operator_checkout_url) - : fallback.operatorCheckoutConfigured, - studioCheckoutConfigured: - manifest?.commerce?.studio_checkout_url != null - ? Boolean(manifest.commerce.studio_checkout_url) - : fallback.studioCheckoutConfigured, - mplSourceConfigured: - manifest?.corresponding_source_url != null - ? Boolean(manifest.corresponding_source_url) - : fallback.mplSourceConfigured, - openSourceRepoConfigured: - manifest?.open_source_repo_url != null - ? Boolean(manifest.open_source_repo_url) - : fallback.openSourceRepoConfigured, - } + return resolveManifestPublicLaunchReadiness(manifest, fallback) +} + +export function resolvePublicLaunchReadinessFromHealth({ + manifest, + health, + fallback = defaultLaunchReadiness, +}: { + manifest?: ReleaseManifestView | null + health?: AuthHealthPayload | null + fallback?: Partial +}): PublicLaunchReadiness { + return resolveHealthPublicLaunchReadiness({ + manifest, + health, + fallback, + }) +} + +export function resolvePublicLaunchStatusSummary( + health?: AuthHealthPayload | null, + manifest?: ReleaseManifestView | null, + fallback: Partial = defaultLaunchReadiness, +): PublicLaunchStatusSummary { + return resolveSharedPublicLaunchStatusSummary({ + health, + manifest, + fallback, + }) +} + +export { + buildPublicLaunchStatusSummary, + normalizePublicLaunchReadiness, } diff --git a/website/src/shared/public-launch.ts b/website/src/shared/public-launch.ts new file mode 100644 index 0000000..eac95f5 --- /dev/null +++ b/website/src/shared/public-launch.ts @@ -0,0 +1,324 @@ +type ReleaseManifestLike = { + commerce?: { + operator_checkout_url?: string | null + studio_checkout_url?: string | null + } | null + corresponding_source_url?: string | null + open_source_repo_url?: string | null + platforms?: Array<{ + platform_key: string + configured: boolean + }> +} + +type AuthHealthLike = { + billing?: { + productPlanMapConfigured?: boolean + pricePlanMapConfigured?: boolean + webhookSecretConfigured?: boolean + } + runtime?: { + public_origin_ready?: boolean + } + launch?: { + posture?: 'launch-ready' | 'preview' + ready?: boolean + readiness?: Partial | null + checklist?: PublicLaunchChecklistItem[] + missingLabels?: string[] + targets?: PublicLaunchTargets + } | null +} + +export type PublicLaunchChecklistId = + | 'windows-download' + | 'macos-download' + | 'linux-download' + | 'operator-checkout' + | 'studio-checkout' + | 'mpl-source' + | 'open-source-notices' + | 'billing-product-map' + | 'billing-price-map' + | 'billing-webhook-secret' + | 'public-auth-runtime' + +export type PublicLaunchChecklistItem = { + id: PublicLaunchChecklistId + label: string + configured: boolean + requiredForPublicLaunch: boolean +} + +export type PublicLaunchReadiness = { + windowsDownloadConfigured: boolean + macosDownloadConfigured: boolean + linuxDownloadConfigured: boolean + operatorCheckoutConfigured: boolean + studioCheckoutConfigured: boolean + mplSourceConfigured: boolean + openSourceRepoConfigured: boolean + billingProductPlanMapConfigured: boolean + billingPricePlanMapConfigured: boolean + billingWebhookSecretConfigured: boolean + publicAuthRuntimeReady: boolean +} + +export type PublicLaunchTargets = { + operatorCheckoutTarget: string | null + studioCheckoutTarget: string | null +} + +export type PublicLaunchStatusSummary = { + posture: 'launch-ready' | 'preview' + ready: boolean + readiness: PublicLaunchReadiness + checklist: PublicLaunchChecklistItem[] + missingLabels: string[] + targets: PublicLaunchTargets +} + +type PublicLaunchReadinessInput = Partial & { + macDownloadConfigured?: boolean +} + +function hasConfiguredPlatform(manifest: ReleaseManifestLike | null | undefined, platformKey: string) { + if (!Array.isArray(manifest?.platforms)) { + return null + } + + return manifest.platforms.some((platform) => platform.platform_key === platformKey && platform.configured) +} + +function normalizeConfiguredValue(value: unknown) { + return value === true +} + +export function normalizePublicLaunchReadiness( + input?: PublicLaunchReadinessInput | null, +): PublicLaunchReadiness { + return { + windowsDownloadConfigured: normalizeConfiguredValue(input?.windowsDownloadConfigured), + macosDownloadConfigured: + normalizeConfiguredValue(input?.macosDownloadConfigured) + || normalizeConfiguredValue(input?.macDownloadConfigured), + linuxDownloadConfigured: normalizeConfiguredValue(input?.linuxDownloadConfigured), + operatorCheckoutConfigured: normalizeConfiguredValue(input?.operatorCheckoutConfigured), + studioCheckoutConfigured: normalizeConfiguredValue(input?.studioCheckoutConfigured), + mplSourceConfigured: normalizeConfiguredValue(input?.mplSourceConfigured), + openSourceRepoConfigured: normalizeConfiguredValue(input?.openSourceRepoConfigured), + billingProductPlanMapConfigured: normalizeConfiguredValue(input?.billingProductPlanMapConfigured), + billingPricePlanMapConfigured: normalizeConfiguredValue(input?.billingPricePlanMapConfigured), + billingWebhookSecretConfigured: normalizeConfiguredValue(input?.billingWebhookSecretConfigured), + publicAuthRuntimeReady: normalizeConfiguredValue(input?.publicAuthRuntimeReady), + } +} + +export function getPublicLaunchChecklist( + readinessInput: PublicLaunchReadinessInput, +): PublicLaunchChecklistItem[] { + const readiness = normalizePublicLaunchReadiness(readinessInput) + + return [ + { + id: 'windows-download', + label: 'Windows download URL', + configured: readiness.windowsDownloadConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'macos-download', + label: 'macOS download URL', + configured: readiness.macosDownloadConfigured, + requiredForPublicLaunch: false, + }, + { + id: 'linux-download', + label: 'Linux download URL', + configured: readiness.linuxDownloadConfigured, + requiredForPublicLaunch: false, + }, + { + id: 'operator-checkout', + label: 'Operator checkout URL', + configured: readiness.operatorCheckoutConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'studio-checkout', + label: 'Studio checkout URL', + configured: readiness.studioCheckoutConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'mpl-source', + label: 'MPL corresponding-source URL', + configured: readiness.mplSourceConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'open-source-notices', + label: 'Open-source repo/notices URL', + configured: readiness.openSourceRepoConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'billing-product-map', + label: 'Billing product-plan map', + configured: readiness.billingProductPlanMapConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'billing-price-map', + label: 'Billing price-plan map', + configured: readiness.billingPricePlanMapConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'billing-webhook-secret', + label: 'Paddle webhook secret', + configured: readiness.billingWebhookSecretConfigured, + requiredForPublicLaunch: true, + }, + { + id: 'public-auth-runtime', + label: 'Public auth runtime posture', + configured: readiness.publicAuthRuntimeReady, + requiredForPublicLaunch: true, + }, + ] +} + +export function getMissingPublicLaunchChecklistItems( + readinessInput: PublicLaunchReadinessInput, + options?: { + requiredOnly?: boolean + }, +): PublicLaunchChecklistItem[] { + return getPublicLaunchChecklist(readinessInput).filter((item) => ( + !item.configured && (!options?.requiredOnly || item.requiredForPublicLaunch) + )) +} + +export function isPublicLaunchReady(readinessInput: PublicLaunchReadinessInput): boolean { + return getMissingPublicLaunchChecklistItems(readinessInput, { requiredOnly: true }).length === 0 +} + +export function resolveManifestPublicLaunchReadiness( + manifest: ReleaseManifestLike | null | undefined, + fallback?: PublicLaunchReadinessInput | null, +): PublicLaunchReadiness { + const normalizedFallback = normalizePublicLaunchReadiness(fallback) + + const windowsDownloadConfigured = hasConfiguredPlatform(manifest, 'windows') + const macosDownloadConfigured = hasConfiguredPlatform(manifest, 'macos') + const linuxDownloadConfigured = hasConfiguredPlatform(manifest, 'linux') + + return { + windowsDownloadConfigured: + windowsDownloadConfigured ?? normalizedFallback.windowsDownloadConfigured, + macosDownloadConfigured: + macosDownloadConfigured ?? normalizedFallback.macosDownloadConfigured, + linuxDownloadConfigured: + linuxDownloadConfigured ?? normalizedFallback.linuxDownloadConfigured, + operatorCheckoutConfigured: + manifest?.commerce?.operator_checkout_url != null + ? Boolean(manifest.commerce.operator_checkout_url) + : normalizedFallback.operatorCheckoutConfigured, + studioCheckoutConfigured: + manifest?.commerce?.studio_checkout_url != null + ? Boolean(manifest.commerce.studio_checkout_url) + : normalizedFallback.studioCheckoutConfigured, + mplSourceConfigured: + manifest?.corresponding_source_url != null + ? Boolean(manifest.corresponding_source_url) + : normalizedFallback.mplSourceConfigured, + openSourceRepoConfigured: + manifest?.open_source_repo_url != null + ? Boolean(manifest.open_source_repo_url) + : normalizedFallback.openSourceRepoConfigured, + billingProductPlanMapConfigured: normalizedFallback.billingProductPlanMapConfigured, + billingPricePlanMapConfigured: normalizedFallback.billingPricePlanMapConfigured, + billingWebhookSecretConfigured: normalizedFallback.billingWebhookSecretConfigured, + publicAuthRuntimeReady: normalizedFallback.publicAuthRuntimeReady, + } +} + +export function resolvePublicLaunchTargets( + manifest: ReleaseManifestLike | null | undefined, + health?: AuthHealthLike | null, +): PublicLaunchTargets { + return { + operatorCheckoutTarget: + health?.launch?.targets?.operatorCheckoutTarget + ?? manifest?.commerce?.operator_checkout_url + ?? null, + studioCheckoutTarget: + health?.launch?.targets?.studioCheckoutTarget + ?? manifest?.commerce?.studio_checkout_url + ?? null, + } +} + +export function resolveHealthPublicLaunchReadiness({ + manifest, + health, + fallback, +}: { + manifest?: ReleaseManifestLike | null + health?: AuthHealthLike | null + fallback?: PublicLaunchReadinessInput | null +}): PublicLaunchReadiness { + const manifestReadiness = resolveManifestPublicLaunchReadiness(manifest, fallback) + const healthReadiness = normalizePublicLaunchReadiness({ + ...manifestReadiness, + billingProductPlanMapConfigured: health?.billing?.productPlanMapConfigured, + billingPricePlanMapConfigured: health?.billing?.pricePlanMapConfigured, + billingWebhookSecretConfigured: health?.billing?.webhookSecretConfigured, + publicAuthRuntimeReady: health?.runtime?.public_origin_ready, + ...health?.launch?.readiness, + }) + + return healthReadiness +} + +export function buildPublicLaunchStatusSummary( + readinessInput: PublicLaunchReadinessInput, + targets: PublicLaunchTargets = { + operatorCheckoutTarget: null, + studioCheckoutTarget: null, + }, +): PublicLaunchStatusSummary { + const readiness = normalizePublicLaunchReadiness(readinessInput) + const ready = isPublicLaunchReady(readiness) + + return { + posture: ready ? 'launch-ready' : 'preview', + ready, + readiness, + checklist: getPublicLaunchChecklist(readiness), + missingLabels: getMissingPublicLaunchChecklistItems(readiness, { + requiredOnly: true, + }).map((item) => item.label), + targets, + } +} + +export function resolvePublicLaunchStatusSummary({ + manifest, + health, + fallback, +}: { + manifest?: ReleaseManifestLike | null + health?: AuthHealthLike | null + fallback?: PublicLaunchReadinessInput | null +}): PublicLaunchStatusSummary { + return buildPublicLaunchStatusSummary( + resolveHealthPublicLaunchReadiness({ + manifest, + health, + fallback, + }), + resolvePublicLaunchTargets(manifest, health), + ) +} diff --git a/website/src/site-config.ts b/website/src/site-config.ts index e7a4646..4918cb2 100644 --- a/website/src/site-config.ts +++ b/website/src/site-config.ts @@ -126,10 +126,15 @@ export const launchReadiness = { operatorCheckoutConfigured: Boolean(operatorCheckoutUrl), studioCheckoutConfigured: Boolean(studioCheckoutUrl), windowsDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'windows')?.configured), + macosDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'macos')?.configured), macDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'macos')?.configured), linuxDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'linux')?.configured), mplSourceConfigured: Boolean(mplSourceUrl), openSourceRepoConfigured: Boolean(openSourceRepoUrl), + billingProductPlanMapConfigured: false, + billingPricePlanMapConfigured: false, + billingWebhookSecretConfigured: false, + publicAuthRuntimeReady: false, } as const export const paddleReadyDescription =