Harden preview-tier website deployment proofs
This commit is contained in:
parent
f2815ba53e
commit
3e5cd793c3
21 changed files with 420 additions and 37 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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__')
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}`)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue