Harden preview-tier website deployment proofs

This commit is contained in:
axiomlogicnexus 2026-06-22 07:53:11 +00:00
parent f2815ba53e
commit 3e5cd793c3
21 changed files with 420 additions and 37 deletions

View file

@ -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:

View file

@ -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`

View file

@ -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

View file

@ -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`

View file

@ -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. |

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <file> --out-dir <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 <file> --identity-file <key>`
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`

View file

@ -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",

View file

@ -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) {

View file

@ -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,

View file

@ -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', () => {

View file

@ -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__')

View file

@ -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'),

View file

@ -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', () => {

View file

@ -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,

View file

@ -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'}`)

View file

@ -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: {

View file

@ -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

View file

@ -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: