Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
CATALYST_WEBHOOK_URL=
DATABASE_URL=
DODOPAY_API_KEY=
DODOPAY_SANDBOX_MODE=
DODOPAY_WEBHOOK_SECRET=
Expand Down Expand Up @@ -27,4 +26,27 @@ VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_MEASUREMENT_ID=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_STORAGE_BUCKET=

# --- Phase 0 security hardening -------------------------------------------
# GitHub webhook HMAC secret (sha256 signatures are verified over the raw body).
# Configure this in your GitHub repo webhook settings; leave empty to reject
# all GitHub webhook deliveries.
GITHUB_WEBHOOK_SECRET=

# Webhook signing secrets for payment gateways (HMAC-SHA256 over raw body).
# Per-gateway value wins over the shared value; unset => deliveries rejected (503).
PAYMENTS_WEBHOOK_SECRET=
PAYMENTS_WEBHOOK_SECRET_2CHECKOUT=
PAYMENTS_WEBHOOK_SECRET_DODOPAY=

# Comma-separated allowlist of valid ``cat_live_...`` API keys. Unset => the
# API-key rate tier is disabled entirely (Phase 1 replaces this with hashed,
# persisted keys).
VALID_API_KEYS=

# Firebase Admin service account for server-side ID token verification
# (state sync auth). Either inline JSON (optionally base64) or a file path.
# Unset => /api/state/sync responds 401 to everyone (fail closed).
FIREBASE_SERVICE_ACCOUNT_JSON=
FIREBASE_SERVICE_ACCOUNT_PATH=
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,9 @@ Thumbs.db
.cache
.temp


# Python tooling artifacts
__pycache__/
*.pyc
.coverage
coverage/
24 changes: 24 additions & 0 deletions .lighthouserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"ci": {
"collect": {
"staticDistDir": "./dist",
"numberOfRuns": 3,
"settings": {
"preset": "desktop"
}
},
"assert": {
"assertions": {
"categories:performance": ["warn", { "minScore": 0.5 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:best-practices": ["error", { "minScore": 0.85 }],
"categories:seo": ["error", { "minScore": 0.85 }],
"resource-summary:script-size": ["off"],
"resource-summary:total-size": ["off"]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
231 changes: 231 additions & 0 deletions CODE_REVIEW.md

Large diffs are not rendered by default.

84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,83 @@
# CatalystLab
# CatalystLab

Website-quality intelligence platform: a Vite 6 + React 18 SPA with an integrated Express 4 server that runs website-audit **engines** against user-supplied URLs, metered by a tiered rate limiter, with Firebase auth/Firestore on the client, MongoDB analytics, and fail-closed payment-gateway integrations.

> Review remediation status: **Phase 0 ✅ · Phase 1 ✅ · Phase 2 ✅ · Phase 3 ✅** — see [`CODE_REVIEW.md`](./CODE_REVIEW.md) for the full audit and roadmap.

## Quickstart

```bash
npm ci
cp .env.example .env # all variables optional — every integration fails closed
GITHUB_WEBHOOK_SECRET=dev-secret npm run dev # Express (port 3000) + Vite middleware
```

- Dev server: `http://localhost:3000` (PORT/HOST honor env — `PORT`/`HOST`).
- Without credentials the app runs in **degraded-but-working mode**: analytics stay in-memory, emails dispatch in Mailgun mock mode, payments return `503 Payments not configured`, and state sync answers `401` until a Firebase service account is configured.

## Scripts

| Command | What it does |
| --- | --- |
| `npm run dev` | tsx server.ts (Express + Vite middleware, HMR off for proxy compatibility) |
| `npm run build` | Production bundle to `dist/` |
| `npm test` | Vitest suite (168 tests: UI + server route suite) |
| `npm run test:coverage` | Same suite with v8 coverage, thresholds gated on `server/**` |
| `npm run lint` | ESLint (0 warnings tolerated) |
| `npm run check:bundle` | Enforce JS bundle-size budgets after `npm run build` |

CI (`.github/workflows/ci.yml`) runs lint, `tsc --noEmit` (**hard gate** — the tree compiles clean under `strict: true`), the test suite, and the production build.

## Environment variables

Everything is optional; behavior when unset is listed. See [`.env.example`](./.env.example) for the full list.

| Group | Variables | Unset behavior |
| --- | --- | --- |
| Firebase (client) | `VITE_FIREBASE_*` | Auth/FS features degrade gracefully |
| Firebase Admin (server) | `FIREBASE_SERVICE_ACCOUNT_JSON` / `_PATH` | `/api/state/sync` → `401` for everyone (fail closed) |
| MongoDB | `MONGODB_URI`, `MONGODB_DB_NAME` | Analytics buffer in memory (zero-cost mode) |
| Mailgun | `MAILGUN_*` | Emails dispatch in **mock mode** (logged, never sent) |
| Payments | `V2CHECKOUT_*`, `DODOPAY_*`, `PAYMENTS_WEBHOOK_SECRET*` | Checkout/verify → `503`; webhooks → `401` |
| GitHub webhooks | `GITHUB_WEBHOOK_SECRET` | All webhook deliveries rejected |
| Rate-limit API keys | `VALID_API_KEYS` (comma-separated `cat_live_…`) | `api_pro` tier disabled entirely |
| Logging | `LOG_LEVEL` (`debug`/`info`/`warn`/`error`), `PORT`, `HOST` | `info` in production, `debug` otherwise |

## Architecture

One HTTP server hosts both the Express API and the SPA (static `dist/` in production, Vite middleware in development). The full diagram and data flows live in [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); the audit-engine inventory is in [`docs/ENGINES.md`](./docs/ENGINES.md).

```
server.ts process entrypoint: HTTP server, Vite/static wiring, PORT/HOST
server/app.ts createApp(): helmet CSP, body limits, identity, routes, 404/errors
server/core/ logger (pino), rate limiter, engine catalog, SSL probe, runtime
server/routes/ telemetry · stateSync · plans · engines · reports · account ·
github · payments · notifications · system · clientLogs
src/lib/serverAuth.ts Firebase ID-token verification → server-derived identity/tiers
lib/engines/ the actual audit-engine implementations (TypeScript)
src/lib/networkSecurity.ts SSRF guard: DNS pinning, private-range blocking, size caps
src/ React SPA (279 files) — pages, components, stores, engines UI
```

## Security model (summary)

- **Identity**: tiers are derived server-side from verified Firebase ID tokens (`firebase-admin`); client headers (`x-user-email`, `subscription-plan`, …) are never trusted. Superadmin requires a signed custom claim.
- **Payments**: fail closed. `/api/payments/verify` never grants entitlements; webhooks require HMAC-SHA256 over the raw body; missing gateway credentials → `503`.
- **Webhooks**: GitHub deliveries verify `x-hub-signature-256` over the raw body; unknown `repoId`s are rejected, never auto-provisioned. Repo secrets are never echoed back to clients.
- **SSRF**: every engine request goes through the guard — scheme allowlist, private/loophead range blocking, DNS resolution pinned to the validated address (anti-rebinding), response-size caps, TLS verification on.
- **Rate limiting**: per-identity daily unit budgets + 60s burst windows, in-memory, keyed by server-derived identity; visitor budget 20 units/day.
- **Secrets hygiene**: the Firebase **web** config (`firebase-applet-config.json`, `VITE_FIREBASE_*`) is public by design — it identifies the Firebase project and is safe to ship in the client bundle. The Admin **service account** is the credential, kept out of Git and provided via `FIREBASE_SERVICE_ACCOUNT_*`.

## Observability

- Server: structured pino logs (JSON lines), one line per request with `x-request-id` correlation, credential-header redaction (`LOG_LEVEL` to tune).
- Client: `src/lib/logger.ts` facade — console passthrough in dev; in production, redacted/deduplicated warn/error batches ship to `POST /api/client-logs` (validated, rate-limited) via `sendBeacon`/`fetch keepalive`.

## Testing

```bash
npm test # 168 tests — UI (jsdom) + server routes (node + supertest)
npm run test:coverage # coverage gate: server/** ≥70% lines/statements/functions
```

The server suite covers every security-critical flow: payment fail-closed behavior, webhook HMAC verification (valid/forged/unknown), identity-spoof resistance, state-sync auth, SSRF blocking, telemetry schema validation, and the client-log sink.
23 changes: 6 additions & 17 deletions api/run-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,15 @@ import path from 'path';

const execFileAsync = promisify(execFile);

const ENGINE_SCRIPT_MAP: Record<string, string> = {
health: 'website_health.py',
latency: 'edge_latency.py',
ai_ready: 'ai_readiness.py',
repo: 'repo_scanner.py',
eco: 'eco_carbon_audit.py',
compliance: 'compliance_risk_audit.py',
migration: 'platform_migration_audit.py',
llmo: 'llmo_optimizer.py'
};
import { ENGINE_SCRIPT_MAP } from '../server/core/enginesCatalog';

export default async function handler(req: any, res: any) {
// Enable CORS for Vercel
res.setHeader('Access-Control-Allow-Credentials', 'true');
// CORS: this endpoint is a public POST API. Credentials are intentionally
// NOT allowed (invalid with a wildcard origin per the fetch spec), and the
// method list is limited to what the endpoint actually serves.
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
);
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key, Authorization');

if (req.method === 'OPTIONS') {
res.status(200).end();
Expand Down
90 changes: 90 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Architecture

CatalystLab is a single-process deployable: one HTTP server hosts the Express API and the React SPA.

```mermaid
flowchart LR
subgraph Browser
SPA["React 18 SPA<br/>(Vite 6 build, code-split routes)"]
SW["Service Worker<br/>(offline asset + telemetry doc cache)"]
CL["logger facade<br/>src/lib/logger.ts"]
end

subgraph Server["Express (server/app.ts) — one process, one port"]
MW["helmet CSP · body limits 256kb/2mb<br/>request-ID logging · attachIdentity"]
subgraph Routes["server/routes/*"]
T["telemetry"]
SS["stateSync"]
EN["engines"]
GH["github"]
PAY["payments"]
ACC["account"]
NOT["notifications"]
SYS["system"]
CLS["clientLogs"]
end
RL["core/rateLimit<br/>daily units + 60s bursts"]
AUTH["lib/serverAuth<br/>firebase-admin verifyIdToken"]
end

subgraph Engines["Audit engines (lib/engines/*)"]
GUARD["networkSecurity SSRF guard<br/>DNS pin + private-range block"]
E1["health · migration · repo-hygiene"]
E2["eco-carbon · compliance · ai-readiness<br/>ai-search · edge-latency"]
end

FB[("Firebase Auth<br/>+ Firestore")]
MG[("MongoDB<br/>time-series analytics")]
MGUN["Mailgun API"]
GW["2Checkout / Dodo<br/>gateways"]
TARGET["Target websites"]
GHAPI["GitHub webhook deliveries"]

SPA -->|"Bearer ID token"| MW
MW --> AUTH --> FB
MW --> RL
MW --> Routes
SPA -->|"audit request"| EN --> GUARD --> TARGET
EN -->|"report"| SPA
T --> MG
SS --> FB
SS --> MG
PAY --> GW
GHAPI -->|"HMAC sha256 over raw body"| GH
NOT --> MGUN
CL -->|"batched warn/error<br/>POST /api/client-logs"| CLS
SW -.-> SPA
```

## Request lifecycle

1. **Transport**: `server.ts` binds `PORT`/`HOST` (env-driven, defaults `3000`/`0.0.0.0`) and mounts either Vite middleware (dev) or static `dist/` (production) behind the same Express app built by `createApp()`.
2. **Security middleware** (in order): helmet CSP — production drops `'unsafe-inline'` by allowlisting startup-computed hashes of the theme-bootstrap script; per-route JSON body limits (256 KB default, 2 MB only for state-sync bulk mutations); structured request logging with `x-request-id` (inbound IDs honored when they match a safe pattern); `attachIdentity` verifies the Firebase ID token once and attaches the server-derived identity (plan, trial, superadmin claim).
3. **Routing**: decomposed route modules register under `/api/*` (+ legacy `/stats/*`, `/telemetry/*`). An API 404 catch-all always answers JSON — the SPA fallback can never shadow an API route. A terminal error handler degrades gracefully when MongoDB is offline.
4. **Rate limiting**: engine-scan routes run `createEngineRateLimitMiddleware`, which resolves the caller's tier (visitor → free/starter/pro/team/enterprise/api_pro → superadmin) from the attached identity, charges units from the daily budget, enforces the 60-second burst window, and answers `429` with a machine-readable envelope.
5. **Engine execution**: `server/routes/engines.ts` validates the target URL, runs the SSRF guard (scheme allowlist, private-range block, DNS resolve → validate → pin the socket to that address), then dispatches to the TypeScript engine in `lib/engines/*` and returns a structured report. Redirects are never auto-followed.

## Identity and trust boundaries

- The **only** server-trusted inputs are: verified Firebase ID tokens (via `firebase-admin`), HMAC-verified webhook payloads, and the constant-time-checked `cat_live_` API-key allowlist (`VALID_API_KEYS`).
- Everything client-supplied — headers like `x-user-email`, plan strings, tier hints — is ignored for authorization. Quota/tier introspection endpoints reflect only the server-derived identity.
- Superadmin is terminal: unlimited budget (`limit: null`, `burstMax: Infinity`), granted exclusively by a signed custom claim on the verified token.

## Telemetry pipeline

First-party, ad-blocker-proof: the SPA and a tiny served script (`/api/telemetry.js`) POST events to `/api/telemetry/event` (also `/api/event`, `/stats/event`). Events are zod-validated and dropped silently when malformed, bot/prefetch traffic is filtered before any processing, geo/UA enrichment is local (`geoip-lite` + `ua-parser-js`), and events queue into MongoDB time-series collections when configured (in-memory otherwise). Query pipelines (`/api/analytics/stats`, `/api/analytics/realtime`, `/api/analytics/anomalies/check`) read the same store.

## Edge mesh (presentation layer)

The dashboard visualizes a 42-PoP edge mesh (`src/lib/edge/pops.ts`) on an interactive cobe globe (`EdgeMeshGlobe`), with plan-tier-driven PoP visibility, projection-based overlay chips, and the telemetry HUD. The mesh is a presentation/simulation feature of the product UI — engine scans execute from this single server process.

## Observability

- **Server**: pino JSON logs; per-request line with method/url/status/duration/requestId; credential headers redacted; level escalates with status (≥500 error, ≥400 warn). `LOG_LEVEL` controls verbosity.
- **Client**: `src/lib/logger.ts` — dev passthrough; production batches redacted, deduplicated warn/error events to `/api/client-logs` (schema-validated, 64 KB cap, 30 req/5 min per identity) with `sendBeacon` on pagehide. Global `error` and `unhandledrejection` hooks plus `ErrorBoundary` report through the same facade.

## Deployment notes

- Single container/process; no serverless entrypoints are load-bearing (`api/*.ts` Vercel twins are dormant; the Express server is canonical).
- `NODE_ENV=production` requires a prior `npm run build` (CSP hashing reads `dist/index.html`).
- All integrations fail closed or degrade to mock modes — a fresh deployment with zero env vars serves the full SPA and public audit surfaces safely.
39 changes: 39 additions & 0 deletions docs/ENGINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Audit engine inventory

CatalystLab runs website-quality audit engines against user-supplied URLs. Every scan passes through the SSRF guard (`src/lib/networkSecurity.ts`) before any outbound byte is sent: scheme allowlist, private/loopback range blocking, DNS resolve → validate → socket pinning (anti-rebinding), response-size caps, TLS verification on, redirects never auto-followed.

## Decision record: `python-engines/`

The original server referenced Python engine scripts (`python-engines/*.py`) that **were never committed to this repository**. During the Phase 2 decomposition the TypeScript implementations in [`lib/engines/`](../lib/engines) were confirmed as the canonical, always-taken code path (the Python dispatch was dead fallback code). The references in `server/core/enginesCatalog.ts` (`ENGINE_SCRIPT_MAP`) are kept as vestigial documentation of the historical naming only; no `.py` file is loaded at runtime. Do not reintroduce a Python engine path without also adding the scripts and a shebang-capable runtime to the deploy image.

## Server engine catalog (`server/core/enginesCatalog.ts`)

Each engine has a canonical id and up to one alias (both resolve to the same implementation).

| Engine id | Alias | Implementation | What it audits |
| --- | --- | --- | --- |
| `health` | `testing_vitals` | `lib/engines/health.ts` | Availability, TLS certificate validity, response status, core health checks |
| `migration` | `planning_arch` | `lib/engines/migration.ts` | Platform/architecture migration readiness (server detection, legacy stack signals) |
| `repo` | `code_quality` | `lib/engines/repo-hygiene.ts` | Repository hygiene surfaces exposed by the site (manifests, source links, metadata) |
| `eco` | `build_eco` | `lib/engines/eco-carbon.ts` | Build/asset carbon efficiency — asset weight rankings, transfer size, carbon estimates |
| `compliance` | `devsecops_compliance` | `lib/engines/compliance.ts` | Security headers, OWASP baseline signals, compliance/privacy markers |
| `ai_ready` | `operations_ai_ready` | `lib/engines/ai-readiness.ts` | AI-discovery readiness (`llms.txt`, structured data, machine-readable metadata) |
| `latency` | `release_edge` | `lib/engines/edge-latency.ts` | Response timing profile and edge-delivery characteristics |
| `ai_search` | — | `lib/engines/ai-search.ts` | AI-search surface readiness (robots directives, crawlable content signals) |
| `llmo` | `evolution_llmo` | *(catalog id; dispatched through the shared pipeline)* | LLMOptimizer-style continuous-evolution scoring |

## Client-side telemetry engines (`src/data/diagnosticEngines.ts`)

Nine client-presented diagnostic engine definitions power the playground and dashboard UI (categories: Performance, Security, SEO, Accessibility, DOM & Vitals, …). They render engine cards, presets, and the interactive playground; network-level checks always execute server-side through the catalog above.

## Execution surfaces

| Route | Purpose |
| --- | --- |
| `POST /api/run-engine` | Run one engine (`engine` + `url`), rate-limited, SSRF-guarded |
| `POST /api/v1/engines/:engine/scan` | Same pipeline under the versioned surface |
| `POST /api/check-url` | Reachability pre-flight through the SSRF guard (no engine run) |
| `POST /api/monitor/probe` | Uptime-style probe used by the monitoring UI |
| `GET /api/master-audit/stream` | SSE stream for the full 8-engine master audit |

Rate costs: a single engine scan costs **1 unit**, a master audit **10 units** (see `server/core/rateLimit.ts` for per-tier daily budgets).
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default tseslint.config(
{
ignores: [
'dist/**',
'coverage/**',
'node_modules/**',
'*.config.js',
'*.config.ts',
Expand Down
Loading
Loading