diff --git a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md index 8f0d27e..19ed77f 100644 --- a/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md +++ b/docs/ops/HYPERTWIST_PUBLIC_WEBSITE_AUTH_BILLING_AND_DISTRIBUTION_PACKET_2026-06-22.md @@ -168,7 +168,7 @@ The frontend behavior coverage now also explicitly pins: - login/register unhappy-path coverage for returned form errors, auth-runtime warning callouts, and OAuth-button visibility/invocation - dashboard launch-readiness visibility plus generated desktop-link verify URL behavior - real `check-runtime-readiness` CLI proof against the checked-in `.env.production.example` files so placeholder launch scaffolds still fail the documented deploy-time command -- spawned `website/server` bootstrap proof from production-shaped same-origin env into live `/health`, `/api/auth/health`, and same-origin built-shell delivery +- spawned `website/server` bootstrap proof from production-shaped same-origin env into live `/health`, `/api/auth/health`, same-origin built-shell delivery, and verified webhook reflection into processed-event health plus persisted billing state The first-party auth server now also supports bounded same-origin public serving for `hypertwist.app` when a built `website/dist/index.html` is present: @@ -279,6 +279,7 @@ Validation result on `2026-06-22`: - the new bounded auth-health probe tests passed, and the protected dashboard type/build surface remained green after adopting the richer health contract - the spawned same-origin auth-server bootstrap proof passed - the real runtime-readiness CLI against the checked-in production example env files failed exactly on the remaining placeholder launch values +- the spawned server also accepted a real signed Paddle test payload and reflected the processed billing event through both `/api/auth/health` and the persisted billing-state file ## Explicit non-claims diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index bd6db9a..3471cbc 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -266,7 +266,7 @@ repo. | 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 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`, and built-shell serving. | | 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, 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, preserves requested platform continuity through `/app/downloads?platform=...`, and surfaces that requested target again after auth handoff inside the protected release lane. 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. | +| 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. Production checkout URLs, secret management, and broader operator/admin billing workflows remain deployment/application tasks, not shipped-code omissions. | | Public open-source notices and corresponding-source surface | Implemented now | first-party `website/` app + `HYPERTWIST_MPL_DISTRIBUTION_PLACEMENT_CHECKLIST_2026-05-25.md` | HyperTwist now has a stable public `Open Source Notices` route linked from pricing, download, and footer surfaces, satisfying the requirement that public distribution surfaces expose notice and corresponding-source guidance when shipped builds contain `MPL`-covered material. The exact public corresponding-source URL still must be configured before external launch. | ### 7. Hyper puzzle topology and non-Euclidean runtime diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index ceaf734..2cb7c51 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -235,7 +235,8 @@ Current consolidated milestone snapshot: `check-runtime-readiness` CLI proof against the checked-in production example env files and spawned `website/server` bootstrap proof from production-shaped same-origin env into live `/health`, `/api/auth/health`, - and built-shell delivery, + built-shell delivery, and verified webhook reflection into live billing + health/state, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin `hypertwist.app` deployment when that build output is present, while the env templates and diff --git a/website/README.md b/website/README.md index 9626591..5c6c8d7 100644 --- a/website/README.md +++ b/website/README.md @@ -106,4 +106,4 @@ The focused frontend test coverage now also pins: - top-level `App` bootstrap coverage for unknown-route redirect and SuperTokens wrapper on/off posture - login/register unhappy-path coverage for returned form errors, auth-runtime warning callouts, and OAuth-button visibility/invocation - dashboard launch-readiness plus desktop-link verify-url behavior -- spawned `website/server` bootstrap proof from production-shaped same-origin env into live `/health`, `/api/auth/health`, and static public/app shell delivery +- spawned `website/server` bootstrap proof from production-shaped same-origin env into live `/health`, `/api/auth/health`, static public/app shell delivery, and verified webhook reflection into billing state diff --git a/website/server/README.md b/website/server/README.md index f6a977d..6a1b2cd 100644 --- a/website/server/README.md +++ b/website/server/README.md @@ -83,7 +83,7 @@ The server now also supports a bounded first-party same-origin deployment mode: - `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 -- a spawned bootstrap test now also proves the real `src/index.ts` process can start from production-shaped same-origin env, reach a ready SuperTokens core, expose green `/api/auth/health`, and serve the built shell +- a spawned bootstrap test now also proves the real `src/index.ts` process can start from production-shaped same-origin env, reach a ready SuperTokens core, expose green `/api/auth/health`, serve the built shell, accept a verified webhook, and reflect that processed billing event through live health plus the persisted entitlement file The website package now also ships a deploy-time verification command: diff --git a/website/server/src/__tests__/server-bootstrap.test.ts b/website/server/src/__tests__/server-bootstrap.test.ts index 972e8a5..5cdb824 100644 --- a/website/server/src/__tests__/server-bootstrap.test.ts +++ b/website/server/src/__tests__/server-bootstrap.test.ts @@ -1,6 +1,7 @@ // @vitest-environment node -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import crypto from 'node:crypto' +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { spawn, type ChildProcessByStdio } from 'node:child_process' import { createServer, type Server } from 'node:http' import { once } from 'node:events' @@ -94,6 +95,15 @@ function readStreamText(value: unknown) { return Buffer.isBuffer(value) ? value.toString('utf8') : String(value || '') } +function buildPaddleSignatureHeader(rawBody: string, secretKey: string, timestamp: string) { + const signature = crypto + .createHmac('sha256', secretKey) + .update(`${timestamp}:${rawBody}`) + .digest('hex') + + return `ts=${timestamp};h1=${signature}` +} + function spawnAuthServer(env: NodeJS.ProcessEnv) { const serverRoot = path.resolve(currentDirectory, '../..') const tsxCliPath = path.join(serverRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs') @@ -295,4 +305,124 @@ describe('website/server bootstrap', () => { await stopChildProcess(child) } }) + + it('applies a verified live webhook and reflects the processed event count through auth health and billing state', async () => { + const coreHost = getNonLoopbackIpv4() + if (!coreHost) { + throw new Error('A non-loopback IPv4 interface is required for public-posture bootstrap proof.') + } + + const root = makeTempRoot() + createDistBundle(root) + const billingStatePath = path.join(root, 'billing-state.json') + const fakeCore = await startFakeCoreServer(coreHost) + const serverPort = await getFreePort('127.0.0.1') + const baseUrl = `http://127.0.0.1:${serverPort}` + const webhookSecret = 'real-secret-value' + + const { child, logs } = spawnAuthServer({ + ...process.env, + PORT: String(serverPort), + SUPERTOKENS_CORE_URI: fakeCore.connectionUri, + API_DOMAIN: 'https://hypertwist.app', + WEBSITE_DOMAIN: 'https://hypertwist.app', + API_BASE_PATH: '/auth', + WEBSITE_BASE_PATH: '/auth', + COOKIE_SECURE: 'true', + SERVE_STATIC_WEBSITE: 'true', + WEBSITE_DIST_PATH: path.join(root, 'dist'), + DEFAULT_PLAN: 'free', + DEFAULT_ROLE: 'operator', + SUPERTOKENS_HEALTH_TIMEOUT_MS: '1500', + DESKTOP_LINK_RATE_LIMIT_MAX: '5', + DESKTOP_LINK_RATE_LIMIT_WINDOW_MS: '900000', + PADDLE_WEBHOOK_SECRET: webhookSecret, + BILLING_STATE_PATH: billingStatePath, + PADDLE_PRODUCT_PLAN_MAP: '{"prod_operator":"operator","prod_studio":"studio"}', + PADDLE_PRICE_PLAN_MAP: '{"pri_operator":"operator","pri_studio":"studio"}', + GITHUB_CLIENT_ID: '', + GITHUB_CLIENT_SECRET: '', + GOOGLE_CLIENT_ID: '', + GOOGLE_CLIENT_SECRET: '', + }) + + try { + await waitForHttpReady(baseUrl, child, logs) + + const rawBody = JSON.stringify({ + event_id: 'evt_live_txn', + event_type: 'transaction.completed', + occurred_at: '2026-06-22T03:45:00.000Z', + data: { + id: 'txn_live_123', + customer_id: 'ctm_live_123', + custom_data: { + email: 'operator@hypertwist.app', + }, + items: [ + { + price: { + id: 'pri_operator', + }, + }, + ], + }, + }) + + const timestamp = String(Math.floor(Date.now() / 1_000)) + const webhookResponse = await fetch(`${baseUrl}/api/billing/paddle/webhook`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'paddle-signature': buildPaddleSignatureHeader(rawBody, webhookSecret, timestamp), + }, + body: rawBody, + }) + + expect(webhookResponse.status).toBe(200) + expect(await webhookResponse.json()).toEqual({ + received: true, + verified: true, + apply_result: { + kind: 'applied', + reason: 'processed', + eventId: 'evt_live_txn', + eventType: 'transaction.completed', + email: 'operator@hypertwist.app', + }, + }) + + const authHealthResponse = await fetch(`${baseUrl}/api/auth/health`) + expect(authHealthResponse.status).toBe(200) + expect(await authHealthResponse.json()).toMatchObject({ + billing: { + processedEventCount: 1, + productPlanMapConfigured: true, + pricePlanMapConfigured: true, + webhookSecretConfigured: true, + }, + fallback: { + active: false, + }, + }) + + const stateFile = JSON.parse(readFileSync(billingStatePath, 'utf8')) + expect(stateFile.processedEvents).toMatchObject({ + evt_live_txn: '2026-06-22T03:45:00.000Z', + }) + expect(stateFile.accounts['operator@hypertwist.app']).toMatchObject({ + email: 'operator@hypertwist.app', + plan: 'operator', + canDownload: true, + accessStatus: 'completed', + source: 'paddle', + customerId: 'ctm_live_123', + transactionId: 'txn_live_123', + lastEventId: 'evt_live_txn', + lastEventType: 'transaction.completed', + }) + } finally { + await stopChildProcess(child) + } + }) })