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
150 changes: 147 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- dev
workflow_dispatch:

concurrency:
Expand Down Expand Up @@ -39,14 +40,157 @@ jobs:
- name: Install website dependencies
run: npm --prefix website ci

- name: Type check
- name: Type check (backend)
run: npx tsc --noEmit

- name: Lint
- name: Lint (backend)
run: npm run lint --if-present

- name: Run tests
- name: Run tests (backend)
run: npm test

- name: Type check (dashboard)
run: npm --prefix dashboard run typecheck

- name: Lint (dashboard)
run: npm --prefix dashboard run lint

- name: Run tests (dashboard)
run: npm --prefix dashboard test

- name: Dep-trail audit (DP6 advisory)
run: ./scripts/check-dep-trail.sh advisory

- name: Build backend, dashboard, and docs
run: npm run build:all

e2e:
name: Playwright (dashboard happy path)
runs-on: ubuntu-latest
needs: validate
timeout-minutes: 20

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: zeroauth_e2e
POSTGRES_USER: zeroauth
POSTGRES_PASSWORD: zeroauth-e2e
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U zeroauth -d zeroauth_e2e"
--health-interval 10s
--health-timeout 5s
--health-retries 10

# NOTE: NODE_ENV is deliberately NOT set at the job level — setting
# NODE_ENV=production makes `npm ci` skip devDependencies (typescript,
# vitest, eslint, etc.), and the build then can't find tsc. We pass
# the server-runtime env only in the "Start backend" step below.
env:
E2E_BASE_URL: http://localhost:3000

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
package-lock.json
dashboard/package-lock.json
website/package-lock.json

- name: Install root dependencies
run: npm ci

- name: Install dashboard dependencies
run: npm --prefix dashboard ci

- name: Install website dependencies
run: npm --prefix website ci

- name: Build everything
run: npm run build:all

- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('dashboard/package-lock.json') }}

- name: Install Playwright browser (chromium)
run: npm --prefix dashboard run e2e:install

- name: Start backend (background)
env:
NODE_ENV: production
PORT: 3000
API_BASE_URL: http://localhost:3000
CORS_ORIGINS: http://localhost:3000
TRUST_PROXY: 'false'
JWT_SECRET: ci-e2e-jwt-secret-not-used-in-production-environments-only
SESSION_SECRET: ci-e2e-session-secret-not-used-in-production-environments-only
ADMIN_API_KEY: ci-e2e-admin-key
ENABLE_DEMO_AUTH: 'false'
LOG_LEVEL: warn
BLOCKCHAIN_RPC_URL: https://sepolia.base.org
BLOCKCHAIN_CHAIN_ID: '84532'
BLOCKCHAIN_PRIVATE_KEY: ''
DID_REGISTRY_ADDRESS: ''
VERIFIER_CONTRACT_ADDRESS: ''
VERIFY_ON_CHAIN: 'false'
ZKP_WASM_PATH: circuits/build/identity_proof_js/identity_proof.wasm
ZKP_ZKEY_PATH: circuits/build/circuit_final.zkey
ZKP_VKEY_PATH: circuits/build/verification_key.json
USE_REDIS_SESSIONS: 'false'
REDIS_URL: redis://localhost:6379
POSTGRES_HOST: localhost
POSTGRES_PORT: '5432'
POSTGRES_DB: zeroauth_e2e
POSTGRES_USER: zeroauth
POSTGRES_PASSWORD: zeroauth-e2e
run: |
node dist/server.js > /tmp/server.log 2>&1 &
echo $! > /tmp/server.pid
# Wait for /api/health to respond
for i in $(seq 1 60); do
if curl -sf http://localhost:3000/api/health > /dev/null; then
echo "server up after ${i}s"
break
fi
sleep 1
done
curl -sf http://localhost:3000/api/health || (cat /tmp/server.log && exit 1)

- name: Run Playwright tests
run: npm --prefix dashboard run e2e

- name: Stop backend
if: always()
run: |
if [ -f /tmp/server.pid ]; then
kill "$(cat /tmp/server.pid)" 2>/dev/null || true
fi

- name: Upload server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: server-log
path: /tmp/server.log
if-no-files-found: ignore

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: dashboard/playwright-report
retention-days: 14
if-no-files-found: ignore
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ coverage/
*.log
docs/missfont.log

# ─── Playwright ─────────────────────────────────────────
dashboard/playwright-report/
dashboard/test-results/
dashboard/.playwright/

# ─── OS / editor ────────────────────────────────────────
.DS_Store
.vscode/
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Live production: <https://zeroauth.dev>. VPS at `104.207.143.14` under user `zer
- **Error handling:** Routes return JSON `{ error: '<machine_code>', message: '<human>' }` with appropriate HTTP status. Sensitive details (DB errors, internal trace) stay in Winston, not in the response.
- **Tests:** Jest. Unit + request-level tests in `tests/*.test.ts`. Currently 50/50 passing. Every new endpoint adds a request-level test before merge.
- **Smart contracts:** Solidity 0.8 via Hardhat; deployed to Base Sepolia (chain 84532).
- **Frontend:** React 19 + Vite for the dashboard; Docusaurus 3 for the docs site.
- **Frontend (developer console / dashboard):** React 19 + Vite 7 + TypeScript strict. Routing via `react-router-dom`. Server state via `@tanstack/react-query`. Styling via Tailwind CSS + a small set of hand-written primitives (`Button`, `Input`, `Card`, `Table`, `Badge`, `Modal`, `Toast`). Unit tests with vitest + @testing-library/react. Lives in `dashboard/`, served as static files by Express at `/dashboard`. The suite's `dashboard_CLAUDE.md` calls for Next.js 15 — see [adr/0002-dashboard-stack-vite-not-nextjs.md](adr/0002-dashboard-stack-vite-not-nextjs.md) for why we deferred that migration.
- **Frontend (marketing site):** Docusaurus 3 for the docs site at `/docs`; vanilla HTML/CSS for the landing page at `/`.
- **Commits:** Plain English subject + body explaining "why". Conventional Commits not enforced.

## Critical language rules (enforce in PR review)
Expand Down
84 changes: 84 additions & 0 deletions adr/0002-dashboard-stack-vite-not-nextjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ADR-0002 — Build the developer console as a Vite + React 19 SPA, not Next.js 15

## Status
Accepted

## Context

The prompt suite's `dashboard_CLAUDE.md` calls for a Next.js 15 App-Router dashboard with server components, server actions, middleware-enforced auth, and Tailwind + shadcn/ui. The repo as it exists today ships a tiny Vite + React 18 single-page app at `dashboard/` that is served as static files by the same Express process that hosts the API.

We need to deliver a high-quality developer console covering every endpoint the central API exposes — overview, API keys, users, devices, verifications, attendance, audit, settings — with unit + integration tests and automated linting. The buyer-facing comparator is Auth0, Clerk, Stytch, WorkOS.

Two paths:

**Path A — adopt the suite's spec literally.** Replace the Vite scaffold with Next.js 15. Migrate the existing Express-mounted dashboard to a separate Next.js server (either co-deployed on the VPS at `:3001` and proxied by Caddy, or replacing the Express static-file mount entirely). Adopt App Router, server components, server actions, middleware, shadcn/ui, React Query.

**Path B — keep Vite, build the same quality bar.** Replace the existing dashboard `App.tsx` with a Vite + React 19 + React Router + React Query + Tailwind app. Tenant scoping is still enforced server-side by Express middleware (`authenticateTenantApiKey`); the SPA only ever reads its own tenant's data via the console JWT. Keep the existing static-file mount so the deploy story is unchanged.

## Decision

**Path B.** Stick with Vite + React 19. Adopt React Router, React Query, Tailwind CSS, vitest + React Testing Library, ESLint 9. Build the entire console surface there.

The suite's `dashboard_CLAUDE.md` is reconciled to match this choice in `CLAUDE.md` at the repo root: the path-mention "Next.js App Router under `/app/`" is replaced with "React Router under `dashboard/src/routes/`", and the `/api/console/*` proxy that Next.js would do via server components is replaced with direct `fetch()` from the SPA to the same Express endpoints with the console JWT in the `Authorization` header.

## Consequences

- **Positive — speed.** The Vite build pipeline already works, deploys via the existing Dockerfile, and lives behind the same Caddy reverse proxy. Adding routing + data fetching + Tailwind is a one-day change; migrating to Next.js is a multi-day change with new build pipeline, new deploy story, new auth-middleware location, and a re-think of how Express co-exists with the Next.js server. DP8 (the 60-day clock) penalises the migration.
- **Positive — single auth layer.** All authorization lives in `src/middleware/tenant-auth.ts` and the console JWT verifier in `src/routes/console.ts`. The dashboard never holds private logic; it sends bearer tokens to the same API every external SDK uses. This matches the suite's standing instruction #1 ("The dashboard reads; it does not own data").
- **Positive — testability.** Vitest + React Testing Library + jsdom is the standard stack for Vite SPAs. Console-API integration tests stay in the root Jest suite using supertest against `createApp()`.
- **Negative — no server components.** Tenant-scoped data goes over the wire to the client, decrypted by the same JWT the client uses. We mitigate by (a) refusing to render anything before the JWT is verified by the API on the first `/api/console/account` call, and (b) keeping every API call tenant-scoped on the server.
- **Negative — initial paint shows the login skeleton briefly.** A Next.js server-component flow could redirect to `/login` before any HTML hits the wire. The SPA momentarily shows an empty layout before deciding to render `<Login />` vs `<App />`. This is a UX cosmetic issue, not a security issue — no tenant data is ever exposed before auth.
- **Negative — owe an exit path.** If the dashboard needs SSR for SEO or first-paint reasons later, we have to migrate. We mark the deferral here so we can revisit during Vanguard-tier hardening.
- **Neutral — no shadcn/ui.** Replaced with Tailwind + a small set of hand-written primitives (`Button`, `Input`, `Card`, `Table`, `Badge`, `Modal`, `Toast`). Total UI primitive surface is ~300 lines; the radix-ui transitive footprint is avoided. If shadcn/ui adoption becomes worth it later, the primitives are a drop-in replacement.

## Alternatives considered (and rejected)

- **Next.js 15 migration today.** Rejected per DP8. Reopen post-Vanguard if SSR becomes load-bearing for buyer demos.
- **Add server-side rendering via Vite SSR.** Considered. The complexity-to-value ratio is poor for a tenant-private dashboard that's not crawled by search engines.
- **Stay on the existing tiny App.tsx and extend it inline.** Rejected. The file is already 520 lines of inline styles; a real console needs routing, data caching, forms with validation, and a primitive UI layer. Going wider on the same file gets us a 5,000-line `App.tsx` that nobody can review.

## Tooling adopted (one batch — see "Consequences" for justification of each)

| Dependency | Workspace | Purpose |
|---|---|---|
| `react-router-dom` | dashboard | Client-side routing |
| `@tanstack/react-query` | dashboard | Server-state caching + dedupe + invalidation |
| `tailwindcss` | dashboard | Utility-first styling |
| `@tailwindcss/vite` | dashboard | Tailwind v4 Vite plugin (replaces postcss/autoprefixer in v4) |
| `@vitest/coverage-v8` | dashboard | V8 coverage reporter for vitest |
| `clsx` | dashboard | Class-name helper for conditional Tailwind |
| `vitest` | dashboard | Unit test runner (matches Vite) |
| `@testing-library/react` | dashboard | Component testing |
| `@testing-library/user-event` | dashboard | User-interaction simulation |
| `@testing-library/jest-dom` | dashboard | DOM matchers |
| `jsdom` | dashboard | DOM for vitest |
| `eslint` | dashboard | Lint, sharing flat-config style with root |
| `eslint-plugin-react-hooks` | dashboard | React-specific lint rules |
| `eslint-plugin-react-refresh` | dashboard | Vite Fast Refresh hygiene |

Supply-chain audit on all the above: `npm audit --omit=dev` is run against the dashboard workspace after install; no high/critical findings in this set as of 2026-05-12.

License survey: all MIT or Apache-2.0.

## Migration plan (in this commit chain)

1. This ADR.
2. Reconcile `CLAUDE.md` at the repo root: dashboard stack section now reads "Vite + React 19 + React Router + React Query + Tailwind + vitest + RTL + ESLint 9".
3. `dashboard/package.json` upgrades: React 18 → 19, Vite 5 → 7, plus the new deps above.
4. Restructure `dashboard/src/` into routes, layout, components, lib, hooks.
5. Reauthor `App.tsx` as a router root. Keep zero behavior from the old single-page admin viewer; that view migrates to the new `/admin` page.
6. Add `vitest.config.ts`, `tailwind.config.ts`, `postcss.config.js`, `eslint.config.js` to dashboard workspace.
7. Build the eight console pages (login, signup, overview, api keys, users, devices, verifications, attendance, audit, settings).
8. Unit tests for primitives + page-level happy paths. Integration tests in root Jest suite for the console JWT flow + tenant scoping.
9. Wire dashboard `npm run lint` + `npm run typecheck` + `npm test` into root CI.

## References

- Suite spec: `zeroauth_prompt_suite/04_development_suite/02_claude_code_dev/CLAUDE_md/dashboard_CLAUDE.md` (Next.js path)
- Suite build prompt: `zeroauth_prompt_suite/04_development_suite/02_claude_code_dev/build_prompts/B05_dashboard_bootstrap.md`
- ADR-0000 grandfather list (current dashboard deps)
- Auth0, Clerk, Stytch, WorkOS — buyer-facing comparators for console UX

---
LAST_UPDATED: 2026-05-12
OWNER: Pulkit Pareek
64 changes: 64 additions & 0 deletions adr/0003-adopt-playwright-for-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ADR-0003 — Adopt Playwright for end-to-end dashboard testing

## Status
Accepted

## Context

ADR-0002 set up the dashboard SPA with vitest + @testing-library/react for unit and component coverage. The suite's `dashboard_CLAUDE.md` and B05 (dashboard bootstrap) call for Playwright on top of that for end-to-end coverage — exercising the full stack against a real browser and a real backend.

Unit + component tests already cover the API client, the format helpers, the primitives, and the Login flow with mocked `fetch`. Those tests run in 5 seconds and catch most regressions. They do **not** catch:

- the dashboard against the real Express server (CORS, helmet headers, the `/dashboard` base path)
- the SPA + console JWT flow against a real Postgres
- a cross-tenant query bug that survives mocking
- a Tailwind class that lints clean but renders wrong
- a router redirect loop introduced by `RequireAuth`

These gaps are exactly what Playwright is meant for.

## Decision

Adopt `@playwright/test` (single dev dependency) for the dashboard workspace. Write one E2E happy-path spec to start: signup → first-key reveal → mint a second key → register a device → see the audit events.

Scope expands over time, but always inside `dashboard/e2e/*.spec.ts`. Edge cases continue to live in vitest + supertest — Playwright is reserved for the journeys that prove the full stack works end-to-end.

## Consequences

- **Positive — flush detection of stack-level bugs.** Anything spanning React → fetch → Express → Postgres → audit row is now covered by one test that runs in CI on every PR.
- **Positive — the developer experience is real.** `npm --prefix dashboard run e2e:ui` opens the Playwright UI for stepping through a failure. `npm --prefix dashboard run e2e` runs headless against a local stack.
- **Negative — CI gets slower.** Playwright adds ~3-5 minutes (browser install + boot time). We mitigate by caching `~/.cache/ms-playwright` keyed on the `@playwright/test` lockfile entry.
- **Negative — flakiness risk.** E2E specs are inherently flakier than unit tests. We mitigate with: `fullyParallel: false`, `workers: 1` in CI, two retries, `trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`. If a spec is consistently flaky, the right answer is to delete + replace with a tighter test — not to disable.
- **Negative — CI needs a real Postgres.** We use GitHub Actions' `services.postgres:` container so the runner brings up an ephemeral 5432 with a fresh DB. No external infra.
- **Neutral — only Chromium.** Tested browsers can grow to Firefox + WebKit later. For the buyer profile (BFSI compliance teams on Windows + Chrome) Chromium covers the highest-value surface today.

## Alternatives considered

- **Cypress.** Decent DX, but the architecture (test runner runs inside the browser) makes interception of multi-tab flows or auth-stateful sessions awkward. Playwright is the modern default.
- **Selenium / WebdriverIO.** Heavier setup, more flakiness, no per-step trace viewer. Rejected.
- **Skip E2E entirely.** The vitest+supertest gates catch most regressions, but the buyer comparator (Auth0, Clerk, Stytch) all ship with E2E coverage for the signup flow. A dashboard that silently breaks at signup is unacceptable.

## Supply chain

- `@playwright/test@^1.60.0` — MIT, maintained by Microsoft, weekly releases, no `npm audit` advisories at this version.
- Bundled browser binaries (Chromium, Firefox, WebKit) installed via `playwright install`. We pin `chromium` only.

## Operational notes

- Local DX: `./scripts/deploy.sh dev` brings up Postgres + Redis + the app on `localhost:3000`. Then `cd dashboard && npm run e2e`.
- CI DX: workflow brings up a Postgres service container, builds the full stack, starts `node dist/server.js` in the background, then runs `npm --prefix dashboard run e2e`.
- The happy path spec creates a tenant whose email is `playwright+<timestamp>-<rand>@example.com` — recognizable for cleanup. In CI it doesn't matter (ephemeral DB). Locally, run:

```sql
DELETE FROM tenants WHERE email LIKE 'playwright+%@example.com';
```

## References

- Suite spec: `zeroauth_prompt_suite/04_development_suite/02_claude_code_dev/CLAUDE_md/dashboard_CLAUDE.md` ("All E2E tests pass (Playwright)")
- B05 build prompt's quality bar
- ADR-0002 (dashboard stack)

---
LAST_UPDATED: 2026-05-12
OWNER: Pulkit Pareek
Loading
Loading