Prove website static routes at HTTP level
This commit is contained in:
parent
bfcad61404
commit
b92627ea04
8 changed files with 164 additions and 19 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
131
website/server/src/__tests__/static-site-routes.test.ts
Normal file
131
website/server/src/__tests__/static-site-routes.test.ts
Normal 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' })
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
23
website/server/src/static-site-routes.ts
Normal file
23
website/server/src/static-site-routes.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue