Prove live website billing webhook reflection
This commit is contained in:
parent
e4fcff160b
commit
114232c579
6 changed files with 138 additions and 6 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue