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