Commit 6c94fc6
authored
## Summary
Replace HashHive's hand-rolled JWT authentication system (jose + bcrypt
+ custom middleware) with [BetterAuth](https://www.better-auth.com/)
database-backed sessions. This eliminates the JWT claim type coercion
bug (#126), adds automatic session refresh, enables immediate session
revocation, and reduces auth maintenance surface by ~60%.
**40 files changed** (+1,143 / -1,157 net) across backend, frontend,
shared schema, tests, and documentation.
## What Changed
### Auth Infrastructure
- **BetterAuth instance** (`packages/backend/src/lib/auth.ts`) --
Drizzle adapter, custom bcrypt hash/verify via `Bun.password`, 8h
session with hourly sliding refresh, self-registration disabled,
air-gapped config (no email verification)
- **Schema** (`packages/shared/src/db/schema.ts`) -- 3 new tables:
`ba_sessions`, `ba_accounts`, `ba_verifications` with `ba_` prefix,
cascade deletes, unique constraint on `(userId, providerId)`
- **Data migration**
(`packages/backend/src/scripts/migrate-auth-accounts.ts`) --
Transaction-wrapped, idempotent script copies bcrypt hashes from
`users.password_hash` to `ba_accounts.password`
### Backend
- **`requireSession`** middleware rewritten -- calls
`auth.api.getSession()` instead of jose JWT validation, validates
`X-Project-Id` header (positive integer only), logs failures via pino
- **RBAC** unchanged -- still validates project membership per-request
via `findProjectMembership()`
- **Auth routes** simplified to `/me` only -- login/logout handled by
BetterAuth at `/api/auth/*`
- **WebSocket auth** uses BetterAuth session cookie only -- removed
`?token=` query param fallback (security improvement)
- **`/projects/select` removed** -- project selection is now client-side
- **`jose` dependency removed** -- all JWT functions (`createToken`,
`validateToken`, `hashPassword`, etc.) deleted
### Frontend
- **Auth store** calls BetterAuth endpoints (`/api/auth/sign-in/email`,
`/api/auth/sign-out`)
- **`syncSelectedProject()`** helper -- auto-selects single project,
clears stale selections on membership change, used in both `login()` and
`fetchUser()`
- **API client** injects `X-Project-Id` header from Zustand
`useUiStore.selectedProjectId`
- **Logout** is resilient to server failures -- clears local state
regardless
- **Login page** navigates explicitly after auto-select (no React
re-render race)
### Documentation
- **`ARCHITECTURE.md`** (new) -- system design, tech stack, schema flow,
API surfaces, data model
- **`docs/development.md`** (new) -- setup, commands, env vars,
infrastructure
- **`docs/testing.md`** (new) -- test strategy, bun:test patterns,
BetterAuth mock patterns
- **`AGENTS.md`** rewritten as thin index pointing to real docs
- **`CONTRIBUTING.md`** enhanced with doc links and issue tracking
section
- **`GOTCHAS.md`** updated for BetterAuth (resolved JWT gotcha, added
new ones)
### Cleanup
- Local tooling configs (`.claude/`, `.cursor/`, `.codex/`,
`.impeccable.md`) untracked from git
- `.gitignore` updated with consolidated AI tool section
## Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| Project selection stays client-side (`X-Project-Id` header) | Multiple
tabs share one session cookie -- server-side `activeProjectId` would
cause tab A's switch to affect tab B |
| No `cookieCache` | Immediate session revocation is more important than
saving a DB lookup for 1-3 users |
| `disableSignUp: true` | Air-gapped lab environment; users created by
admin seed only. BetterAuth's default sign-up would fail on
`users.password_hash` NOT NULL constraint |
| Cookie name `hh.session_token` | Prevents session fixation during
migration from old `session` cookie |
| Custom `Bun.password.hash/verify` | BetterAuth defaults to scrypt;
existing bcrypt `$2b$` hashes must be readable without migration |
| `generateId: false` for user model | Existing `users.id` is `serial`
(integer); BetterAuth defaults to text UUIDs |
## API Changes
| Endpoint | Before | After |
|----------|--------|-------|
| Login | `POST /api/v1/dashboard/auth/login` | `POST
/api/auth/sign-in/email` (BetterAuth) |
| Logout | `POST /api/v1/dashboard/auth/logout` | `POST
/api/auth/sign-out` (BetterAuth) |
| Get session | `GET /api/v1/dashboard/auth/me` | Same (reads BetterAuth
session) |
| Select project | `POST /api/v1/dashboard/projects/select` |
**Removed** -- client-side only |
| Agent auth | `Authorization: Bearer <token>` | **No change** |
## Security Improvements
- **Immediate session revocation** -- deleting DB row invalidates
instantly (vs 24h JWT validity window)
- **Cookie name change** prevents session fixation during migration
- **`?token=` WebSocket fallback removed** -- credentials no longer
appear in logs/history/referrers
- **`BETTER_AUTH_SECRET`** requires 32+ chars (vs 16 for old
`JWT_SECRET`)
- **`X-Project-Id` validated** as positive integer -- rejects 0,
negative, float, non-numeric
- **BetterAuth handler wrapped** in try/catch with error normalization
- **Self-registration disabled** -- prevents unauthorized account
creation on lab network
## Test Coverage
**213 tests passing** (94 backend + 119 frontend), 0 failures.
| Area | Tests | What's Covered |
|------|-------|----------------|
| Auth middleware | 9 | Valid session, null session, getSession throws,
X-Project-Id (valid, missing, malformed x5) |
| Agent token | 5 | No header, non-Bearer, valid, unknown, error state |
| WebSocket auth | 4 | Valid cookie, no auth (4001), no projectIds
(4002), unauthorized projects (4003) |
| Dashboard API guards | 5 | 401 on all protected routes without session
|
| Hash type detection | 2 | Valid MD5, missing hashValue |
| Password hashing | 3 | Hash+verify, reject wrong, bcrypt format check
|
| Frontend login | 5 | Form render, invalid credentials, multi-project
redirect, auto-select, already-authed redirect |
| Frontend select-project | 4 | Render, redirect when selected, select
and redirect, empty state |
## How to Test
```bash
# Run all checks (matches CI)
just ci-check
# Run data migration (after seeding)
bun packages/backend/src/scripts/migrate-auth-accounts.ts
# Manual login flow
# 1. Start services: bun dev
# 2. Navigate to http://localhost:3000/login
# 3. Login with admin@hashhive.local / changeme123
# 4. Verify project auto-select -> dashboard
# 5. Verify sidebar project switch works per-tab
# 6. Verify logout clears session
```
## Post-Deploy Checklist
- [ ] Set `BETTER_AUTH_SECRET` (generate with `openssl rand -base64 32`)
- [ ] Run `bun packages/backend/src/scripts/migrate-auth-accounts.ts` to
copy credentials to `ba_accounts`
- [ ] Verify login works for all existing users
- [ ] Verify agent heartbeats are unaffected
- [ ] Monitor for `AUTH_TOKEN_INVALID` errors in logs (expected for old
cookie cleanup, should stop after first request per user)
- [ ] After 30 days: drop `users.password_hash` column, remove legacy
cookie cleanup code
## Risk Assessment
| Risk | Level | Mitigation |
|------|-------|------------|
| Users locked out after migration | Low | Migration copies existing
bcrypt hashes; custom hash/verify ensures compatibility |
| Multi-tab project corruption | Eliminated | Project selection is
client-side, not in session |
| Agent API regression | None | `requireAgentToken` middleware
completely unchanged |
| Session fixation during transition | Mitigated | Cookie name changed;
legacy cookies auto-cleaned |
| E2E tests break | Fixed | Global setup runs `migrate-auth-accounts.ts`
after seeding |
Closes #126
---
[](https://github.com/EveryInc/compound-engineering-plugin)
---------
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
1 parent 79227a5 commit 6c94fc6
113 files changed
Lines changed: 1862 additions & 13816 deletions
File tree
- .claude
- instincts
- rules
- skills
- api-design
- backend-patterns
- coding-standards
- continuous-learning-v2
- agents
- hooks
- scripts
- database-migrations
- deployment-patterns
- docker-patterns
- e2e-testing
- eval-harness
- frontend-patterns
- iterative-retrieval
- plankton-code-quality
- postgres-patterns
- security-review
- security-scan
- strategic-compact
- tdd-workflow
- verification-loop
- .codex
- .cursor/commands
- .kiro
- hooks
- specs/mern-migration
- steering
- docs
- packages
- backend
- src
- config
- lib
- middleware
- routes/dashboard
- scripts
- services
- tests
- integration
- unit
- frontend
- e2e
- setup
- src
- components/features
- hooks
- lib
- pages
- stores
- tests
- components
- hooks
- mocks
- pages
- utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
0 commit comments