Prove website static routes at HTTP level

This commit is contained in:
axiomlogicnexus 2026-06-22 03:01:48 +00:00
parent bfcad61404
commit b92627ea04
8 changed files with 164 additions and 19 deletions

View file

@ -162,6 +162,7 @@ for `hypertwist.app` when a built `website/dist/index.html` is present:
- SPA fallback remains limited to public/app routes and excludes `/api/*`, `/auth*`, and `/health`
- `WEBSITE_DIST_PATH` and `SERVE_STATIC_WEBSITE` now expose explicit deployment control for that lane
- the runtime-readiness verifier now warns when same-origin public posture leaves that static-serving mode implicit
- request-level server coverage now also proves the live route behavior for public/app routes versus `/api/*`, `/auth*`, `/health`, and missing asset paths
This is browser-based user access for the operator/account surface.

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. |
| 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. |
| 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, and desktop-link verify-url/dashboard readiness behavior. |
| 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. |

View file

@ -226,7 +226,9 @@ Current consolidated milestone snapshot:
`website/dist` bundle with bounded SPA fallback for same-origin `hypertwist.app`
deployment when that build output is present, while the env templates and
runtime-readiness verifier now also make that static-serving posture explicit
instead of leaving it implicit,
instead of leaving it implicit, and request-level server coverage now proves
that public/app routes stay served without shadowing `/api/*`, `/auth*`, or
`/health`,
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

View file

@ -83,6 +83,7 @@ Use the runtime-readiness command before public launch or deployment approval:
- it can optionally verify live `/api/auth/health` posture from the deployed site
- 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
- 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 repo bootstrap CI now also validates this lane through:

View file

@ -82,6 +82,7 @@ The server now also supports a bounded first-party same-origin deployment mode:
- `WEBSITE_DIST_PATH` can override the bundle location when deployment layout differs
- `SERVE_STATIC_WEBSITE=true` forces the server to expect a built bundle, while `SERVE_STATIC_WEBSITE=false` keeps api-only mode explicit
- the example env files now carry those static-serving controls directly so deployment posture is not implicit
- request-level tests now also prove that public/app routes serve the shell while `/api/*`, `/auth*`, `/health`, and missing asset paths remain unshadowed
The website package now also ships a deploy-time verification command:

View file

@ -0,0 +1,131 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:http'
import os from 'node:os'
import path from 'node:path'
import express from 'express'
import { afterEach, describe, expect, it } from 'vitest'
import { registerStaticWebsiteRoutes } from '../static-site-routes'
import type { StaticWebsiteConfig } from '../static-site'
const tempRoots: string[] = []
function makeTempRoot() {
const root = mkdtempSync(path.join(os.tmpdir(), 'hypertwist-static-routes-'))
tempRoots.push(root)
return root
}
function createStaticWebsiteConfig(root: string): StaticWebsiteConfig {
const distPath = path.join(root, 'dist')
mkdirSync(path.join(distPath, 'assets'), { recursive: true })
writeFileSync(
path.join(distPath, 'index.html'),
'<!doctype html><html><body><div id="app">HyperTwist static shell</div></body></html>',
'utf8',
)
writeFileSync(path.join(distPath, 'assets', 'app.js'), 'console.log("ready")', 'utf8')
return {
enabled: true,
distPath,
indexHtmlPath: path.join(distPath, 'index.html'),
reason: 'forced_found',
}
}
async function withServer(
configure: (app: express.Express) => void | Promise<void>,
run: (baseUrl: string) => Promise<void>,
) {
const app = express()
await configure(app)
app.use((req, res) => {
res.status(404).json({ ok: false, path: req.path })
})
const server = createServer(app)
await new Promise<void>((resolve) => server.listen(0, resolve))
const address = server.address()
if (!address || typeof address === 'string') {
server.close()
throw new Error('Failed to bind test server')
}
const baseUrl = `http://127.0.0.1:${address.port}`
try {
await run(baseUrl)
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
})
}
}
afterEach(() => {
while (tempRoots.length > 0) {
const root = tempRoots.pop()
if (root) {
rmSync(root, { recursive: true, force: true })
}
}
})
describe('registerStaticWebsiteRoutes', () => {
it('serves the built shell for public and app routes without shadowing health, api, auth, or missing assets', async () => {
const root = makeTempRoot()
const staticWebsiteConfig = createStaticWebsiteConfig(root)
await withServer(
(app) => {
app.get('/health', (_req, res) => {
res.json({ ok: true, service: 'hypertwist-auth-server' })
})
app.get('/api/auth/health', (_req, res) => {
res.json({ ok: true, service: 'hypertwist-auth-server', runtime: { mode: 'public' } })
})
app.get('/auth', (_req, res) => {
res.status(404).json({ ok: false, route: 'auth-root' })
})
registerStaticWebsiteRoutes(app, staticWebsiteConfig)
},
async (baseUrl) => {
const aboutResponse = await fetch(`${baseUrl}/about`)
expect(aboutResponse.status).toBe(200)
expect(await aboutResponse.text()).toContain('HyperTwist static shell')
const downloadsResponse = await fetch(`${baseUrl}/app/downloads`)
expect(downloadsResponse.status).toBe(200)
expect(await downloadsResponse.text()).toContain('HyperTwist static shell')
const healthResponse = await fetch(`${baseUrl}/health`)
expect(healthResponse.status).toBe(200)
expect(await healthResponse.json()).toEqual({ ok: true, service: 'hypertwist-auth-server' })
const authHealthResponse = await fetch(`${baseUrl}/api/auth/health`)
expect(authHealthResponse.status).toBe(200)
expect(await authHealthResponse.json()).toEqual({
ok: true,
service: 'hypertwist-auth-server',
runtime: { mode: 'public' },
})
const authRootResponse = await fetch(`${baseUrl}/auth`)
expect(authRootResponse.status).toBe(404)
expect(await authRootResponse.json()).toEqual({ ok: false, route: 'auth-root' })
const assetResponse = await fetch(`${baseUrl}/assets/missing.js`)
expect(assetResponse.status).toBe(404)
expect(await assetResponse.json()).toEqual({ ok: false, path: '/assets/missing.js' })
},
)
})
})

View file

@ -15,7 +15,8 @@ import { createBillingStateStore, type BillingPlan, type BillingRole } from './b
import { verifyPaddleWebhookSignature } from './paddle-webhook'
import { getRuntimeConfigDiagnostics } from './runtime-config'
import { buildAllowedOriginMatcher, createDesktopLinkStore } from './security'
import { resolveStaticWebsiteConfig, shouldServeSpaFallback } from './static-site'
import { resolveStaticWebsiteConfig } from './static-site'
import { registerStaticWebsiteRoutes } from './static-site-routes'
const Github = GithubProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GithubProvider>
const Google = GoogleProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GoogleProvider>
@ -422,22 +423,7 @@ app.get('/api/auth/desktop-link/verify', (req, res) => {
})
app.use(errorHandler())
if (staticWebsiteConfig.enabled) {
app.use(express.static(staticWebsiteConfig.distPath, {
fallthrough: true,
index: false,
}))
app.get('*', (req, res, next) => {
if (!shouldServeSpaFallback(req.path)) {
next()
return
}
res.sendFile(staticWebsiteConfig.indexHtmlPath)
})
}
registerStaticWebsiteRoutes(app, staticWebsiteConfig)
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error('[hypertwist-auth-server] unhandled error', error)

View file

@ -0,0 +1,23 @@
import express, { type Express } from 'express'
import type { StaticWebsiteConfig } from './static-site'
import { shouldServeSpaFallback } from './static-site'
export function registerStaticWebsiteRoutes(app: Express, staticWebsiteConfig: StaticWebsiteConfig) {
if (!staticWebsiteConfig.enabled) {
return
}
app.use(express.static(staticWebsiteConfig.distPath, {
fallthrough: true,
index: false,
}))
app.get('*', (req, res, next) => {
if (!shouldServeSpaFallback(req.path)) {
next()
return
}
res.sendFile(staticWebsiteConfig.indexHtmlPath)
})
}