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 8525d12..4c8ca88 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 @@ -379,6 +379,17 @@ sessions to restage the current committed `website/` tree into a temporary VPS checkout, prove the live `3011` behavior, and clean the temp lane back up without rebuilding the SSH/archive/build/start sequence manually. +That deployment helper and the shared runtime-readiness/bundle lane now also +distinguish `launch` from `preview` posture: preview rehearsal can honestly +leave operator checkout, Windows download, release-manifest +version/build/checksum/published-at, and `PADDLE_WEBHOOK_SECRET` values empty +while still rejecting placeholder strings, and the helper can now switch to +`--archive-source worktree` so the real in-progress local packet can be proved +on the host before commit. A later `2026-06-22` preview-tier worktree proof on +the shared VPS passed with `runtime.mode: mixed`, `public_origin_ready: true`, +`webhookSecretConfigured: false`, and all release-manifest platforms still +unconfigured for anonymous viewers. + ## Explicit non-claims This packet does not claim any of the following: 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 32c4f6a..a41af51 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 @@ -96,6 +96,23 @@ when a built website bundle exists: single manifest can own the exact public origin and emit validated frontend env, server env, `systemd`, and `nginx` outputs before installation +## Preview-tier rehearsal posture + +For non-public rehearsal on the shared VPS, the first-party bundle/readiness +lane now also supports an explicit preview tier: + +- set `deploymentTier: "preview"` in the bundle manifest or + `VITE_PUBLIC_DEPLOYMENT_TIER=preview` plus `DEPLOYMENT_TIER=preview` in env +- honest empty operator checkout, Windows download, release-manifest + version/build/checksum/published-at, and `PADDLE_WEBHOOK_SECRET` values now + downgrade to warnings instead of failures +- placeholder strings such as `replace-me-*` still fail in preview posture +- live same-origin preview proof may report `runtime.mode: mixed` when + `public_origin_ready: true` and the only remaining runtime warning is the + loopback SuperTokens-core notice +- the staging helper can validate either committed `HEAD` or the in-progress + local packet through `--archive-source worktree` + ## If a split-host auth topology is chosen later That is allowed, but it should not be the unexamined default. @@ -108,6 +125,19 @@ If a split-host topology is used: - ensure dashboard health reports `public auth origin ready: yes` - ensure launch-readiness warnings are empty before public launch +## Preview-tier staging diagnostics that are acceptable before launch + +Before public launch values are fully available, a non-public preview-tier +proof on the shared VPS is still considered healthy when: + +- runtime mode is `mixed` or `public` +- `public auth origin ready: yes` +- runtime config errors are empty +- the remaining warnings are limited to the loopback SuperTokens-core notice + and the expected preview-tier launch-readiness gaps +- the anonymous release manifest returns `configured=false` and + `download_available=false` for unreleased platforms + ## Current diagnostics that must be green Before public launch, the current runtime and dashboard surfaces should show: @@ -132,6 +162,9 @@ npm run check:runtime-readiness -- --frontend-env .env --server-env server/.env Use `--skip-live-health` when a deployed endpoint is not reachable yet and only env-file posture should be checked. +For non-public preview rehearsal, add `--deployment-tier preview` when the +rendered env or manifest has not already set the tier explicitly. + The repo now also keeps that command honest by testing it directly against the checked-in production example files. @@ -158,6 +191,14 @@ staging proof on `3011` that successfully served `/health`, `/api/auth/health`, `/api/releases/manifest`, and the first-party root shell from a temporary HyperTwist checkout on the real shared host. +That proof set is now wider than the original launch-like rehearsal: a later +`2026-06-22` scripted proof also passed in preview tier while staging the live +local worktree through `--archive-source worktree`, with honest empty operator +checkout, Windows download, release-manifest version/build/checksum/published +fields, and `PADDLE_WEBHOOK_SECRET`, while `/api/auth/health` still reported +`runtime.mode: mixed`, `public_origin_ready: true`, and only the loopback +SuperTokens-core warning. + ## Related authorities - `website/README.md` diff --git a/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md b/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md index 10f8df0..88b6729 100644 --- a/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md +++ b/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md @@ -107,14 +107,23 @@ Fallback direct-edit source: Replace placeholders with real production values before launch. +For non-public rehearsal before those live values exist, the same bundle +renderer now also supports `deploymentTier: "preview"` with honest empty +operator checkout, Windows download, release-manifest +version/build/checksum/published-at, and `server.paddleWebhookSecret` values. +That preview posture is only for staging proof and validation; it does not +replace the real launch values above. + Minimum required live values include: +- `VITE_PUBLIC_DEPLOYMENT_TIER=launch` - `VITE_SUPERTOKENS_API_DOMAIN=https://hypertwist.app` - `VITE_SUPERTOKENS_WEBSITE_DOMAIN=https://hypertwist.app` - `VITE_AUTH_API_BASE_URL=https://hypertwist.app` - real operator checkout URL - real Windows download URL - real corresponding-source URL +- `DEPLOYMENT_TIER=launch` - `API_DOMAIN=https://hypertwist.app` - `WEBSITE_DOMAIN=https://hypertwist.app` - `PORT=3011` @@ -144,12 +153,14 @@ cd /home/dev/src/HyperTwist/website npm run run:vps-same-origin-staging-proof -- \ --manifest deploy/hypertwist.same-origin.bundle.json \ --identity-file /path/to/shared-vps-key \ + --archive-source worktree \ --json ``` Current behavior of that helper: -- archives the current committed `website/` tree +- archives the current committed `website/` tree by default +- can instead archive the live local worktree through `--archive-source worktree` - stages it into a temporary VPS checkout under `/tmp/` - rewrites the manifest for temporary checkout ownership and port `3011` - installs deps, renders the same-origin bundle, builds the frontend, and boots @@ -165,6 +176,9 @@ Latest proof result on `2026-06-22`: - passed on the real shared VPS - confirmed green health/auth/release/shell behavior on `3011` +- preview-tier proof also passed while staging the live local worktree rather + than `HEAD`, with `runtime.mode: mixed`, `public_origin_ready: true`, and no + configured download or webhook-secret release lane yet - left no retained temp checkout afterward ### 4. Install the systemd service diff --git a/docs/ops/HYPERTWIST_WEBSITE_VPS_PRECUTOVER_HOST_STATE_2026-06-22.md b/docs/ops/HYPERTWIST_WEBSITE_VPS_PRECUTOVER_HOST_STATE_2026-06-22.md index 4bebb58..9fa6e94 100644 --- a/docs/ops/HYPERTWIST_WEBSITE_VPS_PRECUTOVER_HOST_STATE_2026-06-22.md +++ b/docs/ops/HYPERTWIST_WEBSITE_VPS_PRECUTOVER_HOST_STATE_2026-06-22.md @@ -136,6 +136,29 @@ Follow-up proof on `2026-06-22`: - on the final cleanup check, no `3011` listener remained and the temp checkout path was absent +Additional preview-tier proof on `2026-06-22`: + +- the scripted helper was rerun in `--archive-source worktree` mode so the + exact in-progress local packet, not just committed `HEAD`, was staged to the + VPS +- the rendered manifest used `deploymentTier=preview` with honest empty + operator checkout, Windows download, release-manifest + version/build/checksum/published-at, and `PADDLE_WEBHOOK_SECRET` values +- `/api/auth/health` still returned `ok: true`, + `runtime.public_origin_ready: true`, and `runtime.mode: mixed` +- the only runtime warning remained the loopback SuperTokens-core notice +- billing health truthfully reported `webhookSecretConfigured: false` +- `/api/releases/manifest` returned `ok: true` with all platform entries still + `configured: false` and `download_available: false` +- cleanup again removed the temporary `3011` listener and the temp checkout + +Operational reading from that second proof: + +- HyperTwist now has a first-party preview-tier rehearsal path for the real + shared host, not only a launch-like proof lane +- the bundle/readiness helpers now align with the live website behavior that + already truthfully distinguishes preview posture from launch-ready posture + ## Related authorities - `docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md` diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index 140dd15..7537efa 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, 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 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. An isolated VPS-local staging proof then also confirmed that the committed HyperTwist website lane can serve green health, release-manifest, and first-party shell responses on that real shared host behind `3011`, and 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 root-owned cutover. | +| Public `hypertwist.app` marketing shell | Implemented now | first-party `website/` app + feature registry/roadmap authority | HyperTwist now has a dedicated first-party public web surface for homepage, about, resources, pricing, download, support, and legal routes. This lane is separate from the embedded Unreal browser runtime under `Content/Browser/` and does not claim browser-simulator parity. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place, bootstrap CI now validates both the frontend and auth-server website commands directly, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin public deployment. 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 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 also 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 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 root-owned cutover. | | Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, billing-secret/map configuration, and local-versus-public runtime deployment posture. Focused frontend coverage now also protects deep-link login redirect preservation, safe `next`-path normalization across auth entry points, fallback/email auth-bootstrap normalization, 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` 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. 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. Both the public and protected download surfaces now also carry first-party rollout steps plus release/notices/source references so the desktop setup lane is more than a generic link bucket, and 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`. 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/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index d2a187c..ff26296 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -276,7 +276,13 @@ Current consolidated milestone snapshot: green health, release-manifest, and first-party shell responses on that real shared host behind `3011`, with that same non-root host-proof flow now codified in a first-party staging-proof helper so future sessions can rerun - the temp checkout/build/boot verification path directly, + the temp checkout/build/boot verification path directly, and the same + bundle/readiness/staging lane now also distinguishes `launch` from `preview` + posture so honest missing checkout/download/webhook/release values are + accepted only for non-public preview rehearsal, `runtime.mode: mixed` with + public-origin readiness is treated as valid preview-host proof, and + `--archive-source worktree` can prove the exact in-progress packet on the + host before commit, and the live public website lane now also owns route-aware title/description/canonical/Open-Graph/Twitter metadata for the real `hypertwist.app` marketing surface while marking protected/auth routes as diff --git a/website/.env.production.example b/website/.env.production.example index 9e8d5f0..f613b88 100644 --- a/website/.env.production.example +++ b/website/.env.production.example @@ -1,3 +1,4 @@ +VITE_PUBLIC_DEPLOYMENT_TIER=launch VITE_SUPERTOKENS_API_DOMAIN=https://hypertwist.app VITE_SUPERTOKENS_WEBSITE_DOMAIN=https://hypertwist.app VITE_SUPERTOKENS_API_BASE_PATH=/auth diff --git a/website/README.md b/website/README.md index 7ba3c87..b5234a1 100644 --- a/website/README.md +++ b/website/README.md @@ -116,6 +116,13 @@ Before public launch, configure: - production Paddle checkout URLs - the public corresponding-source URL for MPL-covered shipped material +For non-public rehearsal, the same bundle/readiness lane also supports an +explicit preview posture through `deploymentTier: "preview"` in the manifest or +`VITE_PUBLIC_DEPLOYMENT_TIER=preview` plus `DEPLOYMENT_TIER=preview` in env. +That preview tier may honestly leave operator checkout, Windows download, and +Paddle webhook-secret values empty while the public launch lane still requires +real values. + Do not launch the public pricing/download pages without a valid open-source notices and corresponding-source destination. @@ -125,12 +132,18 @@ Recommended production posture is documented in: Use the runtime-readiness command before public launch or deployment approval: -- it fails if required public launch values are still missing +- in `launch` tier, it fails if required public launch values are still missing +- in `preview` tier, it accepts honest empty operator checkout, Windows + download, release-manifest, and billing-webhook values as warnings while + still rejecting placeholder strings - it can optionally verify live `/api/auth/health` posture from the deployed site - it now also verifies the anonymous public `GET /api/releases/manifest` route and the deployed website root shell marker - it now fails explicitly when the live origin is still serving the placeholder rollout page instead of the first-party website/auth-server lane +- same-origin preview proof may remain in runtime mode `mixed` when public + origin readiness is true and the only remaining warning is the loopback + SuperTokens-core notice - the auth server can now also serve the built `website/dist` bundle directly for same-origin `hypertwist.app` deployment when that build output is present - the repo now also includes first-party same-origin `nginx` and `systemd` handoff templates under `website/deploy/` @@ -144,7 +157,9 @@ Use the runtime-readiness command before public launch or deployment approval: current host already has FamiliarOS bound to `3001` - the repo now also includes `npm run run:vps-same-origin-staging-proof` so the current committed `website/` tree can be staged into a temporary VPS checkout - and proven on the real shared host before any root-owned live cutover + and proven on the real shared host before any root-owned live cutover, with + optional `--archive-source worktree` support when the proof should validate + the in-progress local packet before commit rather than `HEAD` - it now warns when same-origin public deployment leaves static website serving mode ambiguous - request-level server tests now also pin that same-origin shell behavior instead of relying only on helper-level assertions - the public pricing/download/notices pages now also surface preview-versus-launch posture directly from the same bounded launch checklist diff --git a/website/deploy/README.md b/website/deploy/README.md index f46ccdf..46ddfcf 100644 --- a/website/deploy/README.md +++ b/website/deploy/README.md @@ -27,12 +27,19 @@ Before using these templates: - preferred path: copy `hypertwist.same-origin.bundle.example.json`, replace the placeholder values, and render a validated deployment bundle with `npm run render:same-origin-bundle -- --manifest --out-dir ` +- keep `deploymentTier` on `launch` for public cutover; use `preview` only for + non-public rehearsal, where operator checkout, Windows download, + release-manifest version/build/checksum/published-at values, and + `server.paddleWebhookSecret` may be honestly blank and warn instead of fail + while placeholder strings still remain invalid - before any root-owned cutover, you can now also run `npm run run:vps-same-origin-staging-proof -- --manifest --identity-file ` from the local repo to stage the current committed `website/` tree into a VPS temp checkout, boot it on the host-safe staging port, verify the real `/health`, `/api/auth/health`, `/api/releases/manifest`, and root shell - surfaces, and clean the temp lane back up on success + surfaces, and clean the temp lane back up on success; add + `--archive-source worktree` when the proof should use the in-progress local + worktree rather than committed `HEAD` - set `publicOrigin` to the exact live origin that should own both the website and auth cookies; for the canonical production lane that value is `https://hypertwist.app` diff --git a/website/deploy/hypertwist.same-origin.bundle.example.json b/website/deploy/hypertwist.same-origin.bundle.example.json index 9e5cb72..0764257 100644 --- a/website/deploy/hypertwist.same-origin.bundle.example.json +++ b/website/deploy/hypertwist.same-origin.bundle.example.json @@ -1,4 +1,5 @@ { + "deploymentTier": "launch", "checkoutRoot": "/srv/hypertwist/current", "publicOrigin": "https://hypertwist.app", "serviceUser": "hypertwist", @@ -9,8 +10,8 @@ ], "certificateName": "hypertwist.app", "supportEmail": "hello@hypertwist.app", - "publicDocsUrl": "https://docs.hypertwist.app", - "releaseNotesUrl": "https://notes.hypertwist.app", + "publicDocsUrl": "https://hypertwist.app/resources", + "releaseNotesUrl": "https://hypertwist.app/changelog", "correspondingSourceUrl": "https://hypertwist.app/open-source/replace-me", "openSourceRepoUrl": "https://git.scriptoriumai.io/scriptoriumadmin/hypertwist", "operatorCheckoutUrl": "https://buy.paddle.com/replace-me-operator", diff --git a/website/scripts/check-runtime-readiness.mjs b/website/scripts/check-runtime-readiness.mjs index 5d1f400..ba0a5f3 100644 --- a/website/scripts/check-runtime-readiness.mjs +++ b/website/scripts/check-runtime-readiness.mjs @@ -19,6 +19,7 @@ function parseArgs(argv) { serverEnv: '', healthUrl: '', skipLiveHealth: false, + deploymentTier: '', json: false, } @@ -43,6 +44,11 @@ function parseArgs(argv) { options.skipLiveHealth = true continue } + if (argument === '--deployment-tier') { + options.deploymentTier = argv[index + 1] || '' + index += 1 + continue + } if (argument === '--json') { options.json = true } @@ -113,6 +119,7 @@ const report = buildRuntimeReadinessReport({ frontendEnvPath: frontendEnvFile.path, serverEnvPath: serverEnvFile.path, healthBaseUrl, + deploymentTier: args.deploymentTier, }) if (preloadFailures.length > 0) { diff --git a/website/scripts/render-same-origin-bundle-lib.mjs b/website/scripts/render-same-origin-bundle-lib.mjs index 7c43ace..44f6082 100644 --- a/website/scripts/render-same-origin-bundle-lib.mjs +++ b/website/scripts/render-same-origin-bundle-lib.mjs @@ -51,6 +51,22 @@ function resolvePublicOrigin(manifest = {}, serverNames = []) { return `https://${primaryServerName}` } +function normalizeDeploymentTier(value, fallback = 'launch') { + const normalized = normalizeTrimmed(value).toLowerCase() + if (normalized === 'preview' || normalized === 'launch') { + return normalized + } + return fallback +} + +function requireNonEmptyForTier(value, label, deploymentTier) { + const normalized = normalizeTrimmed(value) + if (!normalized && deploymentTier !== 'preview') { + throw new Error(`${label} is required.`) + } + return normalized +} + function quoteEnvValue(value) { const normalized = String(value ?? '') if (!normalized) { @@ -86,11 +102,13 @@ export function resolveBundleManifest(manifest = {}) { const serviceUser = normalizeTrimmed(manifest.serviceUser || 'hypertwist') const serviceGroup = normalizeTrimmed(manifest.serviceGroup || serviceUser) const serverNames = normalizeServerNames(manifest.serverNames) + const deploymentTier = normalizeDeploymentTier(manifest.deploymentTier || server.deploymentTier, 'launch') return { checkoutRoot: requireNonEmpty(manifest.checkoutRoot, 'checkoutRoot'), serviceUser, serviceGroup, + deploymentTier, publicOrigin: resolvePublicOrigin(manifest, serverNames), serverNames: serverNames.length > 0 ? serverNames : undefined, certificateName: normalizeTrimmed(manifest.certificateName || ''), @@ -99,21 +117,22 @@ export function resolveBundleManifest(manifest = {}) { releaseNotesUrl: requireNonEmpty(manifest.releaseNotesUrl, 'releaseNotesUrl'), correspondingSourceUrl: requireNonEmpty(manifest.correspondingSourceUrl, 'correspondingSourceUrl'), openSourceRepoUrl: requireNonEmpty(manifest.openSourceRepoUrl, 'openSourceRepoUrl'), - operatorCheckoutUrl: requireNonEmpty(manifest.operatorCheckoutUrl, 'operatorCheckoutUrl'), + operatorCheckoutUrl: requireNonEmptyForTier(manifest.operatorCheckoutUrl, 'operatorCheckoutUrl', deploymentTier), studioCheckoutUrl: normalizeTrimmed(manifest.studioCheckoutUrl || ''), planPriceOperator: normalizeTrimmed(manifest.planPriceOperator || 'Launch pricing via Paddle'), planPriceStudio: normalizeTrimmed(manifest.planPriceStudio || 'Contact for launch readiness'), - windowsDownloadUrl: requireNonEmpty(manifest.windowsDownloadUrl, 'windowsDownloadUrl'), - releaseManifestVersion: requireNonEmpty(manifest.releaseManifestVersion, 'releaseManifestVersion'), + windowsDownloadUrl: requireNonEmptyForTier(manifest.windowsDownloadUrl, 'windowsDownloadUrl', deploymentTier), + releaseManifestVersion: requireNonEmptyForTier(manifest.releaseManifestVersion, 'releaseManifestVersion', deploymentTier), releaseManifestChannel: normalizeTrimmed(manifest.releaseManifestChannel || 'preview'), windowsRelease: { - buildId: requireNonEmpty(windowsRelease.buildId, 'windowsRelease.buildId'), - sha256: requireNonEmpty(windowsRelease.sha256, 'windowsRelease.sha256'), + buildId: requireNonEmptyForTier(windowsRelease.buildId, 'windowsRelease.buildId', deploymentTier), + sha256: requireNonEmptyForTier(windowsRelease.sha256, 'windowsRelease.sha256', deploymentTier), fileSizeBytes: String(windowsRelease.fileSizeBytes ?? ''), - publishedAt: requireNonEmpty(windowsRelease.publishedAt, 'windowsRelease.publishedAt'), + publishedAt: requireNonEmptyForTier(windowsRelease.publishedAt, 'windowsRelease.publishedAt', deploymentTier), }, server: { port: normalizeTrimmed(server.port || '3011'), + deploymentTier, superTokensCoreUri: normalizeTrimmed(server.superTokensCoreUri || 'http://127.0.0.1:3567'), apiBasePath: normalizeTrimmed(server.apiBasePath || '/auth'), websiteBasePath: normalizeTrimmed(server.websiteBasePath || '/auth'), @@ -129,7 +148,7 @@ export function resolveBundleManifest(manifest = {}) { healthTimeoutMs: normalizeTrimmed(server.healthTimeoutMs || '2000'), desktopLinkRateLimitMax: normalizeTrimmed(server.desktopLinkRateLimitMax || '5'), desktopLinkRateLimitWindowMs: normalizeTrimmed(server.desktopLinkRateLimitWindowMs || '900000'), - paddleWebhookSecret: requireNonEmpty(server.paddleWebhookSecret, 'server.paddleWebhookSecret'), + paddleWebhookSecret: requireNonEmptyForTier(server.paddleWebhookSecret, 'server.paddleWebhookSecret', deploymentTier), paddleWebhookToleranceMs: normalizeTrimmed(server.paddleWebhookToleranceMs || '5000'), billingStatePath: normalizeTrimmed(server.billingStatePath || '/var/lib/hypertwist/auth/hypertwist-billing-state.json'), paddleProductPlanMap: server.paddleProductPlanMap || {}, @@ -142,6 +161,7 @@ export function buildFrontendEnvEntries(manifest = {}) { const resolved = resolveBundleManifest(manifest) return { + VITE_PUBLIC_DEPLOYMENT_TIER: resolved.deploymentTier, VITE_SUPERTOKENS_API_DOMAIN: resolved.publicOrigin, VITE_SUPERTOKENS_WEBSITE_DOMAIN: resolved.publicOrigin, VITE_SUPERTOKENS_API_BASE_PATH: resolved.server.apiBasePath, @@ -170,6 +190,7 @@ export function buildServerEnvEntries(manifest = {}) { const resolved = resolveBundleManifest(manifest) return { + DEPLOYMENT_TIER: resolved.deploymentTier, PORT: resolved.server.port, SUPERTOKENS_CORE_URI: resolved.server.superTokensCoreUri, API_DOMAIN: resolved.publicOrigin, @@ -219,12 +240,14 @@ export function buildServerEnvEntries(manifest = {}) { } export function buildBundleValidationReport(manifest = {}) { - const frontendEnv = buildFrontendEnvEntries(manifest) - const serverEnv = buildServerEnvEntries(manifest) + const resolved = resolveBundleManifest(manifest) + const frontendEnv = buildFrontendEnvEntries(resolved) + const serverEnv = buildServerEnvEntries(resolved) return buildRuntimeReadinessReport({ frontendEnv, serverEnv, + deploymentTier: resolved.deploymentTier, liveHealth: null, liveHealthAttempted: false, liveReleaseManifest: null, @@ -250,6 +273,7 @@ export function renderSameOriginBundle(manifest = {}) { const validationReport = buildRuntimeReadinessReport({ frontendEnv, serverEnv, + deploymentTier: resolved.deploymentTier, liveHealth: null, liveHealthAttempted: false, liveReleaseManifest: null, diff --git a/website/scripts/render-same-origin-bundle-lib.test.mjs b/website/scripts/render-same-origin-bundle-lib.test.mjs index 5be67a7..9e60b69 100644 --- a/website/scripts/render-same-origin-bundle-lib.test.mjs +++ b/website/scripts/render-same-origin-bundle-lib.test.mjs @@ -14,6 +14,7 @@ import { function createValidManifest() { return { + deploymentTier: 'launch', checkoutRoot: '/srv/hypertwist/current', publicOrigin: 'https://hypertwist.app', supportEmail: 'hello@hypertwist.app', @@ -44,6 +45,35 @@ function createValidManifest() { } } +function createPreviewManifest() { + return { + deploymentTier: 'preview', + checkoutRoot: '/srv/hypertwist/current', + publicOrigin: 'https://hypertwist.app', + supportEmail: 'hello@hypertwist.app', + publicDocsUrl: 'https://hypertwist.app/docs', + releaseNotesUrl: 'https://hypertwist.app/changelog', + correspondingSourceUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + openSourceRepoUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + operatorCheckoutUrl: '', + studioCheckoutUrl: '', + windowsDownloadUrl: '', + releaseManifestVersion: '', + releaseManifestChannel: 'preview', + windowsRelease: { + buildId: '', + sha256: '', + fileSizeBytes: '', + publishedAt: '', + }, + server: { + paddleWebhookSecret: '', + paddleProductPlanMap: {}, + paddlePricePlanMap: {}, + }, + } +} + describe('buildFrontendEnvEntries', () => { it('derives consistent frontend env from the bundle manifest', () => { const env = buildFrontendEnvEntries(createValidManifest()) @@ -97,6 +127,15 @@ describe('buildBundleValidationReport', () => { expect(report.ok).toBe(false) expect(report.failures).toContain('VITE_PADDLE_CHECKOUT_URL_OPERATOR still contains a placeholder value.') }) + + it('accepts preview-tier bundle manifests with honest missing launch values', () => { + const report = buildBundleValidationReport(createPreviewManifest()) + + expect(report.ok).toBe(true) + expect(report.failures).toEqual([]) + expect(report.warnings).toContain('VITE_PADDLE_CHECKOUT_URL_OPERATOR is not set; Operator pricing will stay on the support fallback until checkout is configured.') + expect(report.warnings).toContain('VITE_WINDOWS_DOWNLOAD_URL or WINDOWS_DOWNLOAD_URL is not set; Windows download will remain in preview posture until the release lane is configured.') + }) }) describe('renderSameOriginBundle', () => { @@ -104,6 +143,7 @@ describe('renderSameOriginBundle', () => { const rendered = renderSameOriginBundle(createValidManifest()) expect(rendered.validationReport.ok).toBe(true) + expect(rendered.frontendEnvContent).toContain('VITE_PUBLIC_DEPLOYMENT_TIER=launch') expect(rendered.serverEnvContent).toContain('PORT=3011') expect(rendered.frontendEnvContent).toContain('VITE_WINDOWS_DOWNLOAD_URL=https://downloads.hypertwist.app/windows.exe') expect(rendered.serverEnvContent).toContain('RELEASE_MANIFEST_VERSION=1.0.0') @@ -111,6 +151,16 @@ describe('renderSameOriginBundle', () => { expect(rendered.nginxContent).toContain('server_name hypertwist.app www.hypertwist.app;') expect(rendered.nginxContent).toContain('server 127.0.0.1:3011;') }) + + it('renders a preview-tier bundle without checkout, download, or webhook values', () => { + const rendered = renderSameOriginBundle(createPreviewManifest()) + + expect(rendered.validationReport.ok).toBe(true) + expect(rendered.frontendEnvContent).toContain('VITE_PUBLIC_DEPLOYMENT_TIER=preview') + expect(rendered.frontendEnvContent).toContain('VITE_PADDLE_CHECKOUT_URL_OPERATOR=') + expect(rendered.serverEnvContent).toContain('DEPLOYMENT_TIER=preview') + expect(rendered.serverEnvContent).toContain('PADDLE_WEBHOOK_SECRET=') + }) }) describe('writeSameOriginBundle', () => { diff --git a/website/scripts/run-vps-same-origin-staging-proof-cli.test.mjs b/website/scripts/run-vps-same-origin-staging-proof-cli.test.mjs index d65fc91..d1f6d53 100644 --- a/website/scripts/run-vps-same-origin-staging-proof-cli.test.mjs +++ b/website/scripts/run-vps-same-origin-staging-proof-cli.test.mjs @@ -48,6 +48,8 @@ describe('run-vps-same-origin-staging-proof CLI', () => { manifestPath, '--identity-file', identityFile, + '--archive-source', + 'worktree', '--dry-run', '--json', ], @@ -62,6 +64,7 @@ describe('run-vps-same-origin-staging-proof CLI', () => { const payload = JSON.parse(result.stdout) expect(payload.vpsHost).toBe('212.227.13.220') + expect(payload.archiveSource).toBe('worktree') expect(payload.port).toBe(3011) expect(payload.remoteManifest.server.paddleWebhookSecret).toBe('su***en') expect(payload.remoteScript).toContain('__HYPERTWIST_STAGING_SUMMARY__') diff --git a/website/scripts/run-vps-same-origin-staging-proof-lib.mjs b/website/scripts/run-vps-same-origin-staging-proof-lib.mjs index c568960..096ec6a 100644 --- a/website/scripts/run-vps-same-origin-staging-proof-lib.mjs +++ b/website/scripts/run-vps-same-origin-staging-proof-lib.mjs @@ -34,6 +34,17 @@ function requirePositiveInteger(value, label) { return parsed } +function normalizeArchiveSource(value, fallback = 'git-head') { + const normalized = normalizeTrimmed(value).toLowerCase() + if (normalized === 'worktree') { + return 'worktree' + } + if (normalized === 'git' || normalized === 'head' || normalized === 'git-head') { + return 'git-head' + } + return fallback +} + function maskSecretValue(value) { const normalized = String(value ?? '') if (!normalized) { @@ -71,6 +82,7 @@ export function parseStagingProofArgs(argv) { identityFile: '', vpsHost: '212.227.13.220', vpsUser: 'dev', + archiveSource: 'git-head', remoteDir: '', port: '3011', waitSeconds: '20', @@ -100,6 +112,10 @@ export function parseStagingProofArgs(argv) { options.vpsUser = next index += 1 break + case '--archive-source': + options.archiveSource = next + index += 1 + break case '--remote-dir': options.remoteDir = next index += 1 @@ -148,6 +164,7 @@ export function resolveStagingProofOptions({ identityFile, vpsHost, vpsUser, + archiveSource, remoteDir, port, waitSeconds, @@ -161,6 +178,7 @@ export function resolveStagingProofOptions({ identityFile: path.resolve(requireNonEmpty(identityFile, 'identityFile')), vpsHost: requireNonEmpty(vpsHost || '212.227.13.220', 'vpsHost'), vpsUser: requireNonEmpty(vpsUser || 'dev', 'vpsUser'), + archiveSource: normalizeArchiveSource(archiveSource, 'git-head'), remoteDir: normalizeTrimmed(remoteDir) || buildDefaultRemoteDir(gitRef), port: requirePositiveInteger(port || '3011', 'port'), waitSeconds: requirePositiveInteger(waitSeconds || '20', 'waitSeconds'), diff --git a/website/scripts/run-vps-same-origin-staging-proof-lib.test.mjs b/website/scripts/run-vps-same-origin-staging-proof-lib.test.mjs index 6468ab3..3119e9b 100644 --- a/website/scripts/run-vps-same-origin-staging-proof-lib.test.mjs +++ b/website/scripts/run-vps-same-origin-staging-proof-lib.test.mjs @@ -45,6 +45,7 @@ describe('resolveStagingProofOptions', () => { identityFile: '/tmp/key', vpsHost: '212.227.13.220', vpsUser: 'dev', + archiveSource: 'worktree', remoteDir: '', port: '3011', waitSeconds: '30', @@ -55,11 +56,31 @@ describe('resolveStagingProofOptions', () => { }) expect(options.remoteDir).toBe('/tmp/hypertwist-website-staging-abc1234') + expect(options.archiveSource).toBe('worktree') expect(options.port).toBe(3011) expect(options.waitSeconds).toBe(30) expect(options.keepRemoteDir).toBe(true) expect(options.json).toBe(true) }) + + it('falls back to git-head when the archive source is omitted or unrecognized', () => { + const options = resolveStagingProofOptions({ + manifestPath: 'deploy/hypertwist.same-origin.bundle.json', + identityFile: '/tmp/key', + vpsHost: '212.227.13.220', + vpsUser: 'dev', + archiveSource: 'unexpected-value', + remoteDir: '', + port: '3011', + waitSeconds: '30', + keepRemoteDir: false, + json: false, + dryRun: false, + gitRef: 'abc1234', + }) + + expect(options.archiveSource).toBe('git-head') + }) }) describe('buildRemoteStagingManifest', () => { diff --git a/website/scripts/run-vps-same-origin-staging-proof.mjs b/website/scripts/run-vps-same-origin-staging-proof.mjs index 41860f7..1de3c8e 100644 --- a/website/scripts/run-vps-same-origin-staging-proof.mjs +++ b/website/scripts/run-vps-same-origin-staging-proof.mjs @@ -34,6 +34,31 @@ function runCommand(command, args, options = {}) { return result } +function buildArchiveBuffer(repoRoot, archiveSource) { + if (archiveSource === 'worktree') { + return execFileSync('tar', [ + '--exclude=website/node_modules', + '--exclude=website/dist', + '--exclude=website/coverage', + '--exclude=website/server/node_modules', + '--exclude=website/server/dist', + '-cf', + '-', + 'website', + ], { + cwd: repoRoot, + encoding: null, + maxBuffer: 64 * 1024 * 1024, + }) + } + + return execFileSync('git', ['archive', '--format=tar', 'HEAD', 'website'], { + cwd: repoRoot, + encoding: null, + maxBuffer: 64 * 1024 * 1024, + }) +} + function main() { const rawArgs = parseStagingProofArgs(process.argv.slice(2)) const gitRef = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { @@ -46,6 +71,7 @@ function main() { identityFile: rawArgs.identityFile, vpsHost: rawArgs.vpsHost, vpsUser: rawArgs.vpsUser, + archiveSource: rawArgs.archiveSource, remoteDir: rawArgs.remoteDir, port: rawArgs.port, waitSeconds: rawArgs.waitSeconds, @@ -78,6 +104,7 @@ function main() { manifestPath, vpsHost: options.vpsHost, vpsUser: options.vpsUser, + archiveSource: options.archiveSource, remoteDir: options.remoteDir, port: options.port, waitSeconds: options.waitSeconds, @@ -99,11 +126,7 @@ function main() { return } - const archiveBuffer = execFileSync('git', ['archive', '--format=tar', 'HEAD', 'website'], { - cwd: repoRoot, - encoding: null, - maxBuffer: 64 * 1024 * 1024, - }) + const archiveBuffer = buildArchiveBuffer(repoRoot, options.archiveSource) const stageResult = runCommand('ssh', buildSshArgs({ identityFile: options.identityFile, diff --git a/website/scripts/runtime-readiness-lib.mjs b/website/scripts/runtime-readiness-lib.mjs index f96ac01..4307ea7 100644 --- a/website/scripts/runtime-readiness-lib.mjs +++ b/website/scripts/runtime-readiness-lib.mjs @@ -5,6 +5,14 @@ function normalizeTrimmed(value) { return String(value || '').trim() } +function normalizeDeploymentTier(value, fallback = 'launch') { + const normalized = normalizeTrimmed(value).toLowerCase() + if (normalized === 'preview' || normalized === 'launch') { + return normalized + } + return fallback +} + function collapseWhitespace(value) { return normalizeTrimmed(value).replace(/\s+/g, ' ') } @@ -209,7 +217,7 @@ function requireAbsoluteUrlCandidate(bucket, label, value) { return parsed } -function evaluateFrontendConfig(frontendEnv, serverEnv) { +function evaluateFrontendConfig(frontendEnv, serverEnv, deploymentTier = 'launch') { const bucket = createBucket() const superTokensApiDomain = requireAbsoluteUrl(bucket, 'VITE_SUPERTOKENS_API_DOMAIN', frontendEnv.VITE_SUPERTOKENS_API_DOMAIN) @@ -217,10 +225,22 @@ function evaluateFrontendConfig(frontendEnv, serverEnv) { const authApiBaseUrl = requireAbsoluteUrl(bucket, 'VITE_AUTH_API_BASE_URL', frontendEnv.VITE_AUTH_API_BASE_URL) const windowsDownloadCandidate = resolveEnvCandidate(frontendEnv, serverEnv, 'VITE_WINDOWS_DOWNLOAD_URL', 'WINDOWS_DOWNLOAD_URL') - requireAbsoluteUrlCandidate(bucket, windowsDownloadCandidate.label, windowsDownloadCandidate.value) + if (!normalizeTrimmed(windowsDownloadCandidate.value)) { + if (deploymentTier === 'preview') { + pushWarning(bucket, `${windowsDownloadCandidate.label} is not set; Windows download will remain in preview posture until the release lane is configured.`) + } else { + requireAbsoluteUrlCandidate(bucket, windowsDownloadCandidate.label, windowsDownloadCandidate.value) + } + } else { + requireAbsoluteUrlCandidate(bucket, windowsDownloadCandidate.label, windowsDownloadCandidate.value) + } - if (!requireNonEmpty(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR', frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR)) { - // already recorded as failure + if (!normalizeTrimmed(frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR)) { + if (deploymentTier === 'preview') { + pushWarning(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR is not set; Operator pricing will stay on the support fallback until checkout is configured.') + } else { + pushFailure(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR is missing.') + } } else if (!tryParseUrl(frontendEnv.VITE_PADDLE_CHECKOUT_URL_OPERATOR)) { pushFailure(bucket, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR must be an absolute URL.') } else { @@ -275,7 +295,7 @@ function evaluateFrontendConfig(frontendEnv, serverEnv) { } } -function evaluateServerConfig(serverEnv) { +function evaluateServerConfig(serverEnv, deploymentTier = 'launch') { const bucket = createBucket() const apiDomain = requireAbsoluteUrl(bucket, 'API_DOMAIN', serverEnv.API_DOMAIN) @@ -288,7 +308,11 @@ function evaluateServerConfig(serverEnv) { } if (!normalizeTrimmed(serverEnv.PADDLE_WEBHOOK_SECRET)) { - pushFailure(bucket, 'PADDLE_WEBHOOK_SECRET is missing.') + if (deploymentTier === 'preview') { + pushWarning(bucket, 'PADDLE_WEBHOOK_SECRET is not set; billing webhook handling will remain in preview posture until the live secret is configured.') + } else { + pushFailure(bucket, 'PADDLE_WEBHOOK_SECRET is missing.') + } } else { rejectPlaceholderValue(bucket, 'PADDLE_WEBHOOK_SECRET', serverEnv.PADDLE_WEBHOOK_SECRET) } @@ -476,9 +500,17 @@ export function buildRuntimeReadinessReport({ frontendEnvPath = '', serverEnvPath = '', healthBaseUrl = '', + deploymentTier = '', }) { - const frontend = evaluateFrontendConfig(frontendEnv, serverEnv) - const server = evaluateServerConfig(serverEnv) + const resolvedDeploymentTier = normalizeDeploymentTier( + deploymentTier + || frontendEnv.VITE_PUBLIC_DEPLOYMENT_TIER + || serverEnv.DEPLOYMENT_TIER, + 'launch', + ) + + const frontend = evaluateFrontendConfig(frontendEnv, serverEnv, resolvedDeploymentTier) + const server = evaluateServerConfig(serverEnv, resolvedDeploymentTier) const failures = [...frontend.failures, ...server.failures] const warnings = [...frontend.warnings, ...server.warnings] @@ -496,17 +528,21 @@ export function buildRuntimeReadinessReport({ if (liveHealth.runtime?.public_origin_ready !== true) { failures.push('Live auth health reports public auth origin not ready.') } - if (liveHealth.runtime?.mode !== 'public') { - failures.push(`Live auth health reports runtime mode '${liveHealth.runtime?.mode || 'unknown'}' instead of 'public'.`) + if (liveHealth.runtime?.mode !== 'public' && liveHealth.runtime?.mode !== 'mixed') { + failures.push(`Live auth health reports runtime mode '${liveHealth.runtime?.mode || 'unknown'}' instead of 'public' or 'mixed'.`) } for (const message of liveHealth.runtime?.errors || []) { failures.push(`Live runtime error: ${message}`) } for (const message of liveHealth.runtime?.warnings || []) { - failures.push(`Live runtime warning: ${message}`) + warnings.push(`Live runtime warning: ${message}`) } if (liveHealth.billing?.webhookSecretConfigured !== true) { - failures.push('Live auth health reports Paddle webhook secret missing.') + if (resolvedDeploymentTier === 'preview') { + warnings.push('Live auth health reports Paddle webhook secret missing; billing webhook handling remains in preview posture.') + } else { + failures.push('Live auth health reports Paddle webhook secret missing.') + } } if (liveHealth.billing?.productPlanMapConfigured !== true && liveHealth.billing?.pricePlanMapConfigured !== true) { warnings.push('Live auth health reports no Paddle product/price map configured; custom_data-based resolution remains the only billing-plan path.') @@ -560,6 +596,7 @@ export function buildRuntimeReadinessReport({ frontendEnvPath, serverEnvPath, healthBaseUrl, + deploymentTier: resolvedDeploymentTier, liveHealthChecked: Boolean(liveHealth), liveReleaseManifestChecked: Boolean(liveReleaseManifest), liveWebsiteShellChecked: Boolean(liveWebsiteShell), @@ -578,6 +615,7 @@ export function formatRuntimeReadinessReport(report) { if (report.healthBaseUrl) { lines.push(`Health base URL: ${report.healthBaseUrl}`) } + lines.push(`Deployment tier: ${report.deploymentTier || 'launch'}`) lines.push(`Live health checked: ${report.liveHealthChecked ? 'yes' : 'no'}`) lines.push(`Live release manifest checked: ${report.liveReleaseManifestChecked ? 'yes' : 'no'}`) lines.push(`Live website shell checked: ${report.liveWebsiteShellChecked ? 'yes' : 'no'}`) diff --git a/website/scripts/runtime-readiness-lib.test.mjs b/website/scripts/runtime-readiness-lib.test.mjs index b154423..189a14f 100644 --- a/website/scripts/runtime-readiness-lib.test.mjs +++ b/website/scripts/runtime-readiness-lib.test.mjs @@ -119,6 +119,7 @@ describe('buildRuntimeReadinessReport', () => { it('passes a fully configured public same-origin posture', () => { const report = buildRuntimeReadinessReport({ frontendEnv: { + VITE_PUBLIC_DEPLOYMENT_TIER: 'launch', VITE_SUPERTOKENS_API_DOMAIN: 'https://hypertwist.app', VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'https://hypertwist.app', VITE_AUTH_API_BASE_URL: 'https://hypertwist.app', @@ -129,6 +130,7 @@ describe('buildRuntimeReadinessReport', () => { VITE_OPEN_SOURCE_REPO_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', }, serverEnv: { + DEPLOYMENT_TIER: 'launch', API_DOMAIN: 'https://hypertwist.app', WEBSITE_DOMAIN: 'https://hypertwist.app', SUPERTOKENS_CORE_URI: 'https://auth-core.internal', @@ -158,6 +160,48 @@ describe('buildRuntimeReadinessReport', () => { expect(report.failures).toEqual([]) }) + it('accepts preview deployment posture with missing checkout, download, and webhook values', () => { + const report = buildRuntimeReadinessReport({ + frontendEnv: { + VITE_PUBLIC_DEPLOYMENT_TIER: 'preview', + VITE_SUPERTOKENS_API_DOMAIN: 'https://hypertwist.app', + VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'https://hypertwist.app', + VITE_AUTH_API_BASE_URL: 'https://hypertwist.app', + VITE_MPL_SOURCE_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + VITE_OPEN_SOURCE_REPO_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + }, + serverEnv: { + DEPLOYMENT_TIER: 'preview', + API_DOMAIN: 'https://hypertwist.app', + WEBSITE_DOMAIN: 'https://hypertwist.app', + SUPERTOKENS_CORE_URI: 'http://127.0.0.1:3567', + COOKIE_SECURE: 'true', + }, + liveHealth: { + supertokens: { ready: true }, + fallback: { active: false }, + runtime: { + public_origin_ready: true, + mode: 'mixed', + errors: [], + warnings: ['SUPERTOKENS_CORE_URI still targets a loopback/local-development host.'], + }, + billing: { + webhookSecretConfigured: false, + productPlanMapConfigured: false, + pricePlanMapConfigured: false, + }, + }, + }) + + expect(report.ok).toBe(true) + expect(report.failures).toEqual([]) + expect(report.warnings).toContain('VITE_WINDOWS_DOWNLOAD_URL or WINDOWS_DOWNLOAD_URL is not set; Windows download will remain in preview posture until the release lane is configured.') + expect(report.warnings).toContain('VITE_PADDLE_CHECKOUT_URL_OPERATOR is not set; Operator pricing will stay on the support fallback until checkout is configured.') + expect(report.warnings).toContain('PADDLE_WEBHOOK_SECRET is not set; billing webhook handling will remain in preview posture until the live secret is configured.') + expect(report.warnings).toContain('Live runtime warning: SUPERTOKENS_CORE_URI still targets a loopback/local-development host.') + }) + it('warns when same-origin public posture leaves static website serving ambiguous', () => { const report = buildRuntimeReadinessReport({ frontendEnv: { @@ -235,6 +279,35 @@ describe('buildRuntimeReadinessReport', () => { expect(report.failures).toContain('PADDLE_PRICE_PLAN_MAP still contains a placeholder value.') }) + it('still fails placeholder commercial values during preview posture', () => { + const report = buildRuntimeReadinessReport({ + frontendEnv: { + VITE_PUBLIC_DEPLOYMENT_TIER: 'preview', + 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_MPL_SOURCE_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + VITE_OPEN_SOURCE_REPO_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist', + }, + serverEnv: { + DEPLOYMENT_TIER: 'preview', + 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', + }, + 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.') + }) + it('accepts server-backed release-manifest download and source posture when frontend fallback URLs are absent', () => { const report = buildRuntimeReadinessReport({ frontendEnv: { diff --git a/website/server/.env.production.example b/website/server/.env.production.example index 3f22752..c08e4bd 100644 --- a/website/server/.env.production.example +++ b/website/server/.env.production.example @@ -1,4 +1,5 @@ -PORT=3001 +DEPLOYMENT_TIER=launch +PORT=3011 SUPERTOKENS_CORE_URI=http://127.0.0.1:3567 API_DOMAIN=https://hypertwist.app WEBSITE_DOMAIN=https://hypertwist.app @@ -22,8 +23,8 @@ 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"} SUPPORT_EMAIL=hello@hypertwist.app -PUBLIC_DOCS_URL=https://docs.hypertwist.app -RELEASE_NOTES_URL=https://notes.hypertwist.app +PUBLIC_DOCS_URL=https://hypertwist.app/resources +RELEASE_NOTES_URL=https://hypertwist.app/changelog MPL_SOURCE_URL=https://hypertwist.app/open-source/replace-me OPEN_SOURCE_REPO_URL=https://git.scriptoriumai.io/scriptoriumadmin/hypertwist RELEASE_MANIFEST_VERSION=replace-me-release-version diff --git a/website/server/README.md b/website/server/README.md index 3ed8f0d..26b8827 100644 --- a/website/server/README.md +++ b/website/server/README.md @@ -99,6 +99,12 @@ The server now also supports a bounded first-party same-origin deployment mode: `npm run run:vps-same-origin-staging-proof` so the same bundle manifest can be exercised through a temporary VPS checkout on the real shared host before root-owned service/vhost cutover +- the same bundle/readiness lane now distinguishes `launch` from `preview` + posture, so non-public same-origin rehearsal may keep runtime mode `mixed` + with the loopback SuperTokens warning and an empty `PADDLE_WEBHOOK_SECRET` + while public launch still requires real checkout/download/webhook values +- the staging-proof helper now also supports `--archive-source worktree` when + operators need real host proof for the exact in-progress packet before commit The website package now also ships a deploy-time verification command: