# Multi-User Management & Rate Limiting

## TL;DR

> **Quick Summary**: Replace single-password auth with multi-user system (admin/user roles, self-registration + admin approval), add per-user rate limiting (RTPM/RTPD, token count, credit/quota, concurrent requests) integrated into dashboardGuard middleware.

> **Deliverables**:
> - Users table + bcrypt auth + JWT session (replaces INITIAL_PASSWORD-only)
> - Self-registration with admin approval flow
> - API keys owned by users (userId FK)
> - 4 rate-limit strategies: request-rate, token-count, credit/quota, concurrent
> - Admin dashboard: user list, approve users, set limits per user
> - Vitest test infrastructure setup

> **Estimated Effort**: Large
> **Parallel Execution**: YES - 4 waves
> **Critical Path**: DB schema → Auth system → Rate limiting middleware → Dashboard UI → Integration

---

## Context

### Original Request
User wants user management with API key ownership and rate limiting via token, credit, and other mechanisms to prevent abuse.

### Interview Summary
**Key Discussions**:
- **User Model**: Multi-user with admin/user roles. Fresh start auth (no backward compat).
- **First Admin**: Auto-created from `INITIAL_PASSWORD` env when users table empty.
- **API Keys**: Drop & recreate `apiKeys` with required `userId` FK.
- **Registration**: Public endpoint, requires admin approval before active.
- **Default Limits**: Strict — 100 req/min, 1M tokens/month, $5 credit, 5 concurrent.
- **Rate Limiting**: All 4 strategies (request-rate, token-count, credit/quota, concurrent).
- **Dashboard**: Full UI for admin to manage users and limits.
- **Test Strategy**: TDD — setup vitest infrastructure first.

**Research Findings**:
- `dashboardGuard.js` already has clean auth middleware chain — extend at `canAccessPublicLlmApi()`
- `usageRepo.js` already records `apiKey` per request in `usageHistory` + `usageDaily`
- `apiKeysRepo.js` exists with full CRUD — needs userId FK
- DB uses repo pattern with auto-sync schema from declarative `TABLES` object in `schema.js`
- No test infrastructure — needs vitest from scratch
- UI uses Tailwind CSS with custom components

---

## Work Objectives

### Core Objective
Replace single-password login with multi-user auth (admin/user roles), implement per-user rate limiting across 4 dimensions (request-rate, token-count, credit/quota, concurrent requests), and provide admin dashboard for user/limit management.

### Concrete Deliverables
- `src/lib/db/repos/usersRepo.js` — User CRUD operations
- `src/lib/db/repos/rateLimitRepo.js` — Rate limit config + enforcement queries
- Updated `src/lib/db/repos/apiKeysRepo.js` — Now with userId FK
- Updated `src/lib/db/schema.js` — users, rateLimits tables + apiKeys migration
- Updated `src/lib/db/migrations/002-users-rate-limit.js` — Migration file
- `src/lib/auth/password.js` — bcrypt hash/verify utilities
- `src/lib/auth/jwt.js` — JWT sign/verify (new tokens)
- `src/lib/auth/middleware.js` — Rate limit enforcement middleware
- Updated `src/dashboardGuard.js` — Multi-user auth + rate limit check
- `src/app/api/auth/register/route.js` — Self-registration endpoint
- `src/app/api/auth/login/route.js` — New multi-user login
- `src/app/api/users/route.js` + `[id]/route.js` — Admin user CRUD
- `src/app/api/users/[id]/approve/route.js` — Admin approval
- `src/app/api/users/[id]/limits/route.js` — Rate limit config per user
- `src/app/(dashboard)/dashboard/users/page.js` — User list page
- `src/app/(dashboard)/dashboard/users/[id]/page.js` — User detail + limits
- `src/app/register/page.js` — Public registration page
- Updated `src/app/login/page.js` — New multi-user login
- `vitest.config.ts` + test files

### Definition of Done
- [ ] `vitest run` passes all tests
- [ ] First admin auto-created on startup
- [ ] Self-registration works, new users are pending until approved
- [ ] Admin can list, approve, set limits for any user
- [ ] Rate limits enforced in middleware (all 4 strategies)
- [ ] API keys belong to users, deleted when user is deleted
- [ ] Dashboard shows per-user usage stats

### Must Have
- Multi-user auth with bcrypt + JWT
- Admin role with full user management
- Self-registration with admin approval flow
- 4 rate-limit strategies: RTPM/RTPD, token count, credit/quota, concurrent
- Rate limit middleware in dashboardGuard
- Admin dashboard UI for user + limit management
- Vitest test infrastructure with TDD

### Must NOT Have (Guardrails)
- No backward compatibility with old single-password auth
- No email verification (out of scope — admin approval replaces it)
- No password reset via email (can be added later)
- No OAuth/OIDC provider (out of scope)
- No per-model rate limits (per-user only)
- No organization/team hierarchy (flat users)
- No billing/invoicing (just credit quota, no payment integration)

---

## Verification Strategy (MANDATORY)

> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed.

### Test Decision
- **Infrastructure exists**: NO
- **Automated tests**: TDD
- **Framework**: vitest (Next.js compatible, fast)
- **TDD**: Each task follows RED (failing test) → GREEN (minimal impl) → REFACTOR

### QA Policy
Every task MUST include agent-executed QA scenarios.
Evidence saved to `.omo/evidence/task-{N}-{scenario-slug}.{ext}`.

- **API/Backend**: Use Bash (curl) — Send requests, assert status + response fields
- **Library/Module**: Use Bash (node REPL) — Import, call functions, compare output
- **Frontend/UI**: Use Playwright — Navigate, interact, assert DOM, screenshot

---

## Execution Strategy

### Parallel Execution Waves

```
Wave 1 (Start Immediately - foundation + scaffolding):
├── Task 1: Vitest infrastructure setup [quick]
├── Task 2: Database schema — users + rateLimits tables [quick]
├── Task 3: Database migration 002 [quick]
├── Task 4: Password utils (bcrypt) [quick]
├── Task 5: JWT utils (sign/verify) [quick]
├── Task 6: usersRepo.js — CRUD operations [quick]
└── Task 7: rateLimitRepo.js — config + enforcement queries [quick]

Wave 2 (After Wave 1 - auth + middleware):
├── Task 8: User registration endpoint + tests [unspecified-high]
├── Task 9: User login endpoint + tests [unspecified-high]
├── Task 10: Admin seed (first admin from INITIAL_PASSWORD) [quick]
├── Task 11: Update apiKeysRepo — add userId FK + ownership [unspecified-high]
├── Task 12: Rate limit enforcement middleware [deep]
├── Task 13: Update dashboardGuard — multi-user auth + rate limit gate [deep]
└── Task 14: Auth status endpoint [quick]

Wave 3 (After Wave 2 - admin API + dashboard UI):
├── Task 15: Admin users CRUD API [unspecified-high]
├── Task 16: Admin user approval API [quick]
├── Task 17: Admin rate limits config API [unspecified-high]
├── Task 18: Admin users dashboard page [visual-engineering]
├── Task 19: Admin user detail + limits page [visual-engineering]
├── Task 20: Update login page for multi-user [visual-engineering]
└── Task 21: Registration page [visual-engineering]

Wave 4 (After Wave 3 - integration + polish):
├── Task 22: Update Sidebar nav + layout [quick]
├── Task 23: User self-service: view own keys + usage [unspecified-high]
├── Task 24: Rate limit test — all 4 strategies integrated [deep]
└── Task 25: End-to-end flow tests [deep]

Wave FINAL (After ALL tasks — 4 parallel reviews):
├── Task F1: Plan compliance audit
├── Task F2: Code quality review
├── Task F3: Real manual QA
└── Task F4: Scope fidelity check

Critical Path: Task 1 → 2 → 6 → 12 → 13 → 15 → 18 → 24 → F1-F4
Parallel Speedup: ~65% faster than sequential
Max Concurrent: 7 (Wave 1)
```

### Dependency Matrix

- **1**: - - 2-7, 1
- **2-7**: 1 - 8-14, 1
- **6**: 2, 3 - 8, 10, 11, 2
- **7**: 2, 3 - 12, 2
- **8**: 4, 5, 6 - 15, 2
- **9**: 4, 5, 6 - 13, 2
- **10**: 6 - 13, 2
- **11**: 6 - 15, 17, 2
- **12**: 7, 13 - 24, 2
- **13**: 9, 10, 12 - 15-17, 24, 2
- **14**: 5, 6 - 15, 2
- **15-17**: 8, 11, 13, 14 - 18, 19, 3
- **18**: 15 - 22, 3
- **19**: 15, 17 - 22, 3
- **20**: 9 - 22, 3
- **21**: 8 - 22, 3
- **22**: 18, 19, 20, 21 - 25, 4
- **23**: 11, 13 - 25, 4
- **24**: 12, 13 - 25, 4
- **25**: 22, 23, 24 - F1-F4, 4

### Agent Dispatch Summary

- **1**: **7** - T1-T7 → all `quick`
- **2**: **7** - T8-T9 → `unspecified-high`, T10 → `quick`, T11 → `unspecified-high`, T12-T13 → `deep`, T14 → `quick`
- **3**: **7** - T15 → `unspecified-high`, T16 → `quick`, T17 → `unspecified-high`, T18-T19 → `visual-engineering`, T20-T21 → `visual-engineering`
- **4**: **4** - T22 → `quick`, T23 → `unspecified-high`, T24-T25 → `deep`
- **FINAL**: **4** - F1-F4

---

## TODOs

> Implementation + Test = ONE Task. Never separate.
> EVERY task MUST have: Recommended Agent Profile + QA Scenarios.
> Task labels: bare numbers. Final Wave: F1-F4.

---

### Wave 1 — Foundation & Scaffolding (ALL parallel)

- [ ] 1. Set up vitest test infrastructure

  **What to do**:
  - Install vitest as dev dependency
  - Create `vitest.config.ts` at project root (Next.js compatible, path aliases matching tsconfig)
  - Configure: include `src/**/*.test.{js,ts}`, environment `node` for backend tests
  - Add `"test": "vitest run"` and `"test:watch": "vitest"` scripts to `package.json`
  - Create `src/__tests__/setup.js` with global test setup (mock env, isolated DB path)
  - Create smoke test `src/__tests__/vitest.test.js` asserting `true === true`

  **Must NOT do**: Don't install jest or bun test. Don't create tests for existing code.

  **Recommended Agent Profile**:
  - **Category**: `quick` — Standard dev infrastructure, no complex logic.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 1 (with Tasks 2-7)
  - **Blocks**: Tasks 8-9, 12-13, 15-17, 24-25 (all tasks needing vitest)
  - **Blocked By**: None

  **References**:
  - `package.json` — Match existing scripts format + dependency patterns
  - `tsconfig.json` — Extract path aliases (e.g., `@/` → `./src/`) for vitest `resolve.alias`
  - Vitest docs: `https://vitest.dev/config/` — `resolve.alias`, `test.include`, `test.environment`

  **Acceptance Criteria**:
  - [ ] `vitest.config.ts` exists at project root
  - [ ] `npm run test` executes `vitest run` — exit 0
  - [ ] Smoke test at `src/__tests__/vitest.test.js` passes (1 test)

  **QA Scenarios**:
  ```
  Scenario: Vitest smoke test passes
    Tool: Bash
    Preconditions: vitest installed, config created, smoke test written
    Steps:
      1. Run: npx vitest run
      2. Check exit code is 0
      3. Check output contains "Tests  1 passed"
    Expected Result: Exit 0, "1 passed" in output
    Failure Indicators: Non-zero exit, "0 passed", or "FAIL" in output
    Evidence: .omo/evidence/task-1-smoke.txt

  Scenario: Vitest detects failures correctly
    Tool: Bash
    Preconditions: Create temp failing test
    Steps:
      1. Write test `expect(1).toBe(2)` to temp file
      2. Run: npx vitest run temp-file.test.js
      3. Check exit code non-zero
      4. Delete temp file
    Expected Result: Exit 1, output shows 1 failed
    Evidence: .omo/evidence/task-1-fail.txt
  ```

  **Evidence**: `.omo/evidence/task-1-smoke.txt`, `.omo/evidence/task-1-fail.txt`
  **Commit**: YES — `chore: set up vitest test infrastructure`
  **Files**: `vitest.config.ts`, `package.json`, `src/__tests__/vitest.test.js`, `src/__tests__/setup.js`

- [ ] 2. Define users + rateLimits tables in schema.js

  **What to do**:
  - Open `src/lib/db/schema.js` — find the `TABLES` object that auto-generates schema
  - Add `users` table: id (TEXT PRIMARY KEY, uuid), username (TEXT UNIQUE NOT NULL), password_hash (TEXT NOT NULL), role (TEXT DEFAULT 'user', CHECK role IN ('admin','user')), is_active (INTEGER DEFAULT 1), is_approved (INTEGER DEFAULT 0), created_at (TEXT DEFAULT CURRENT_TIMESTAMP)
  - Add `rateLimits` table: id (TEXT PRIMARY KEY, uuid), userId (TEXT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE), rtpMinute (INTEGER DEFAULT 100), rtpDay (INTEGER DEFAULT 100000), tokenLimitMonthly (INTEGER DEFAULT 1000000), creditLimit (REAL DEFAULT 5.0), maxConcurrent (INTEGER DEFAULT 5), created_at, updated_at
  - Add `userId` column to existing `apiKeys` definition (TEXT REFERENCES users(id) ON DELETE CASCADE) — this modifies the existing auto-schema

  **Must NOT do**:
  - Don't drop or recreate existing tables (apiKeys, usageHistory, usageDaily remain)
  - Don't change any column types in existing tables
  - Don't add indexes yet (schema.js TABLE definitions handle auto-indexing)

  **Recommended Agent Profile**:
  - **Category**: `quick` — Declarative schema addition, follow existing patterns exactly.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1, 3-7)
  - **Parallel Group**: Wave 1
  - **Blocks**: Tasks 6 (usersRepo), 7 (rateLimitRepo), 3 (migration)
  - **Blocked By**: None (but run after Task 1 to have vitest ready)

  **References**:
  - `src/lib/db/schema.js` — Read full file. Find exact `TABLES` object structure, column type patterns (TEXT, INTEGER, REAL), constraint patterns (CHECK, UNIQUE, REFERENCES). Match style exactly.
  - `src/lib/db/repos/` — Skim any repo to see how they reference column names (camelCase from schema.js columns)

  **Acceptance Criteria**:
  - [ ] `users` table defined in TABLES object with all 6 columns
  - [ ] `rateLimits` table defined with all 7 columns + FK to users
  - [ ] `apiKeys` table has new `userId` column with FK reference
  - [ ] App starts without SQL errors (schema auto-syncs on boot)

  **QA Scenarios**:
  ```
  Scenario: Tables exist in SQLite after schema sync
    Tool: Bash (node REPL)
    Preconditions: App started once so schema sync ran
    Steps:
      1. Run: node -e "const db = require('./src/lib/db/index.js'); const tables = db.prepare('SELECT name FROM sqlite_master WHERE type=\\'table\\'').all(); console.log(JSON.stringify(tables));"
      2. Check output array contains {name: 'users'} entry
      3. Check output array contains {name: 'rateLimits'} entry
    Expected Result: Output includes 'users' and 'rateLimits' table names
    Failure Indicators: Missing table names in output, or require throws error
    Evidence: .omo/evidence/task-2-tables.txt

  Scenario: apiKeys has userId column
    Tool: Bash (node REPL)
    Preconditions: Schema synced
    Steps:
      1. Run: node -e "const db = require('./src/lib/db/index.js'); const cols = db.prepare('PRAGMA table_info(apiKeys)').all(); console.log(JSON.stringify(cols));"
      2. Check one column has name 'userId'
    Expected Result: Column with name 'userId' present in apiKeys pragma output
    Evidence: .omo/evidence/task-2-apikeys-col.txt
  ```

  **Evidence**: `.omo/evidence/task-2-tables.txt`, `.omo/evidence/task-2-apikeys-col.txt`
  **Commit**: YES — `feat(db): add users and rateLimits tables, userId FK to apiKeys`
  **Files**: `src/lib/db/schema.js`

- [ ] 3. Create database migration 002

  **What to do**:
  - Check `src/lib/db/migrations/` for existing migration pattern (if any)
  - Create `src/lib/db/migrations/002-users-rate-limit.js` with up/down functions
  - Up: CREATE TABLE IF NOT EXISTS users, CREATE TABLE IF NOT EXISTS rateLimits, ALTER TABLE apiKeys ADD COLUMN userId
  - If migrations don't exist yet as a pattern, create the file to match any existing migration runner convention. If no migration system exists, create this as a run-once SQL script that checks for table existence before creating.
  - Test: verify the migration runs without errors, verify idempotent (run twice = no error)

  **Must NOT do**:
  - Don't drop existing data
  - Don't create a full migration framework if none exists — just a script the app can run

  **Recommended Agent Profile**:
  - **Category**: `quick` — SQL migration script, follow existing patterns.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1-2, 4-7)
  - **Parallel Group**: Wave 1
  - **Blocks**: None directly (schema.js auto-sync is primary), but useful for CI/test setup
  - **Blocked By**: Task 2 (needs schema definition)

  **References**:
  - `src/lib/db/schema.js` — Column names + types to replicate in migration
  - `src/lib/db/migrations/` — Check for existing migration files/pattern
  - `src/lib/db/index.js` — How the DB is initialized (better-sqlite3, connection pattern)

  **Acceptance Criteria**:
  - [ ] Migration file creates all 3 table changes idempotently
  - [ ] Running migration twice produces no errors
  - [ ] Migration can be executed via node script

  **QA Scenarios**:
  ```
  Scenario: Migration runs successfully
    Tool: Bash (node)
    Preconditions: Fresh DB (or test DB path)
    Steps:
      1. Run: node src/lib/db/migrations/002-users-rate-limit.js
      2. Check exit code is 0
    Expected Result: Exit 0, no errors
    Evidence: .omo/evidence/task-3-migrate.txt

  Scenario: Migration is idempotent
    Tool: Bash (node)
    Preconditions: Migration already ran once
    Steps:
      1. Run migration again: node src/lib/db/migrations/002-users-rate-limit.js
      2. Check exit code is 0
      3. Check no duplicate column/table errors
    Expected Result: Exit 0, no errors on second run
    Evidence: .omo/evidence/task-3-idempotent.txt
  ```

  **Evidence**: `.omo/evidence/task-3-migrate.txt`, `.omo/evidence/task-3-idempotent.txt`
  **Commit**: YES — `feat(db): add migration 002 for users and rate limits`
  **Files**: `src/lib/db/migrations/002-users-rate-limit.js`

- [ ] 4. Create password utilities (bcrypt hash/verify)

  **What to do**:
  - Install bcryptjs as dependency (pure JS, no native deps, cross-platform)
  - Create `src/lib/auth/password.js` with two exports: `hashPassword(plain)` → string, `verifyPassword(plain, hash)` → boolean
  - Use 12 salt rounds (standard for bcrypt)
  - Write unit test `src/lib/auth/password.test.js`: test hash produces different string, test verify matches, test verify rejects wrong password

  **Must NOT do**:
  - Don't use native bcrypt (bcrypt package) — use bcryptjs for zero native dep issues
  - Don't add timing-safe comparison (bcryptjs handles this internally)

  **Recommended Agent Profile**:
  - **Category**: `quick` — Simple wrappers around library functions, standard patterns.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1-3, 5-7)
  - **Parallel Group**: Wave 1
  - **Blocks**: Tasks 8 (register), 9 (login), 10 (admin seed)
  - **Blocked By**: Task 1 (vitest needed for test)

  **References**:
  - bcryptjs docs: `https://www.npmjs.com/package/bcryptjs` — `hashSync`, `compareSync` API
  - `src/lib/` — Check existing lib module patterns (how they export, error handling conventions)

  **Acceptance Criteria**:
  - [ ] `hashPassword("test")` returns string ≠ input, starts with `$2a$` or `$2b$`
  - [ ] `verifyPassword("test", hash)` returns `true`
  - [ ] `verifyPassword("wrong", hash)` returns `false`
  - [ ] `hashPassword("")` throws or returns rejected
  - [ ] Unit test `npm run test -- password.test.js` passes 3 tests

  **QA Scenarios**:
  ```
  Scenario: Password hash and verify cycle works
    Tool: Bash (node REPL)
    Preconditions: bcryptjs installed, module created
    Steps:
      1. Run: node -e "const {hashPassword, verifyPassword} = require('./src/lib/auth/password'); const h = hashPassword('supersecret'); console.log('HASHED:' + h.substring(0,10)); console.log('VERIFY:' + verifyPassword('supersecret', h)); console.log('REJECT:' + verifyPassword('wrong', h));"
      2. Check output contains "HASHED:$2"
      3. Check output contains "VERIFY:true"
      4. Check output contains "REJECT:false"
    Expected Result: HASHED starts with $2, VERIFY=true, REJECT=false
    Evidence: .omo/evidence/task-4-hash-verify.txt

  Scenario: Empty password is rejected
    Tool: Bash (node REPL)
    Steps:
      1. Run: node -e "const {hashPassword} = require('./src/lib/auth/password'); try { hashPassword(''); console.log('NO_ERROR'); } catch(e) { console.log('REJECTED:' + e.message.substring(0,30)); }"
    Expected Result: Output contains "REJECTED:"
    Evidence: .omo/evidence/task-4-empty-reject.txt
  ```

  **Evidence**: `.omo/evidence/task-4-hash-verify.txt`, `.omo/evidence/task-4-empty-reject.txt`
  **Commit**: YES — `feat(auth): add bcrypt password hash/verify utilities`
  **Files**: `src/lib/auth/password.js`, `src/lib/auth/password.test.js`, `package.json`

- [ ] 5. Create JWT utilities (sign/verify)

  **What to do**:
  - Install jsonwebtoken as dependency
  - Create `src/lib/auth/jwt.js` with: `signToken(payload)` → string, `verifyToken(token)` → payload | null
  - Use `JWT_SECRET` from `process.env` with fallback to a dev default
  - Token expiry: 7 days
  - Payload: `{ userId, username, role }`
  - Write unit test `src/lib/auth/jwt.test.js`: test sign produces string, test verify returns payload, test expired token returns null, test tampered token returns null

  **Must NOT do**:
  - Don't hardcode secrets in source (always from env)
  - Don't use HS256 with weak keys (enforce minimum 32-char secret)

  **Recommended Agent Profile**:
  - **Category**: `quick` — Standard JWT sign/verify wrappers.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1-4, 6-7)
  - **Parallel Group**: Wave 1
  - **Blocks**: Tasks 8 (register), 9 (login), 13 (dashboardGuard), 14 (status endpoint)
  - **Blocked By**: Task 1 (vitest)

  **References**:
  - jsonwebtoken docs: `https://www.npmjs.com/package/jsonwebtoken` — `sign()`, `verify()` API
  - `.env.example` — Check if `JWT_SECRET` already exists, add if not
  - `src/dashboardGuard.js` — Current auth pattern (how tokens are read from cookies, how they're used)

  **Acceptance Criteria**:
  - [ ] `signToken({userId: "u1", username: "test", role: "user"})` returns non-empty string
  - [ ] `verifyToken(validToken)` returns object with userId, username, role
  - [ ] `verifyToken("garbage")` returns null (no throw)
  - [ ] `verifyToken(expiredToken)` returns null
  - [ ] Unit test passes 4 tests

  **QA Scenarios**:
  ```
  Scenario: JWT sign and verify roundtrip
    Tool: Bash (node REPL)
    Preconditions: JWT_SECRET set in env, module created
    Steps:
      1. Run: node -e "process.env.JWT_SECRET='test-secret-at-least-32-chars!!'; const {signToken, verifyToken} = require('./src/lib/auth/jwt'); const t = signToken({userId:'abc',username:'test',role:'user'}); console.log('TOKEN:' + (t.length > 20)); console.log('PAYLOAD:' + JSON.stringify(verifyToken(t)));"
      2. Check TOKEN:true (token is long string)
      3. Check PAYLOAD contains userId:'abc', username:'test', role:'user'
    Expected Result: TOKEN:true, PAYLOAD:{"userId":"abc","username":"test","role":"user"...}
    Evidence: .omo/evidence/task-5-roundtrip.txt

  Scenario: Invalid token returns null
    Tool: Bash (node REPL)
    Steps:
      1. Run: node -e "process.env.JWT_SECRET='test-secret-at-least-32-chars!!'; const {verifyToken} = require('./src/lib/auth/jwt'); console.log('RESULT:' + verifyToken('not.a.valid.jwt'));"
    Expected Result: RESULT:null (or RESULT:undefined)
    Evidence: .omo/evidence/task-5-invalid.txt
  ```

  **Evidence**: `.omo/evidence/task-5-roundtrip.txt`, `.omo/evidence/task-5-invalid.txt`
  **Commit**: YES — `feat(auth): add JWT sign/verify utilities`
  **Files**: `src/lib/auth/jwt.js`, `src/lib/auth/jwt.test.js`, `.env.example`, `package.json`

- [ ] 6. Create usersRepo.js — CRUD operations

  **What to do**:
  - Create `src/lib/db/repos/usersRepo.js` following existing repo patterns
  - Functions: `createUser(username, passwordHash)`, `findByUsername(username)`, `findById(id)`, `listUsers()`, `updateApproval(id, isApproved)`, `updateRole(id, role)`, `deleteUser(id)`, `markActive(id, active)`
  - Use uuid for id generation (check if uuid package already used in codebase)
  - Use parameterized queries only (no string interpolation)
  - Write unit test `src/lib/db/repos/usersRepo.test.js`: test create, find by username, find by id, list, CRUD cycle

  **Must NOT do**:
  - Don't handle password hashing in repo (that's auth layer)
  - Don't create transaction wrappers unless existing repos use them

  **Recommended Agent Profile**:
  - **Category**: `quick` — Data access layer following existing repo patterns.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1-5, 7)
  - **Parallel Group**: Wave 1
  - **Blocks**: Tasks 8-11, 15-16
  - **Blocked By**: Task 2 (schema must exist), Task 1 (vitest)

  **References**:
  - `src/lib/db/repos/apiKeysRepo.js` — Exact repo pattern to follow: how DB is imported, function signature style, error handling, export style
  - `src/lib/db/schema.js` — Column names for users table (exact casing)
  - `src/lib/db/index.js` — How the DB singleton is imported across repos

  **Acceptance Criteria**:
  - [ ] All 8 CRUD functions implemented
  - [ ] All use parameterized queries
  - [ ] `createUser` generates UUID id
  - [ ] `findByUsername` returns null for non-existent user (not throw)
  - [ ] Unit test passes 5+ tests

  **QA Scenarios**:
  ```
  Scenario: Create and retrieve user
    Tool: Bash (node REPL)
    Preconditions: DB synced with users table, test DB path
    Steps:
      1. Run: node -e "const repo = require('./src/lib/db/repos/usersRepo'); const u = repo.createUser('qa_test', 'hash123'); console.log('CREATED:' + JSON.stringify({id:u.id, username:u.username})); const found = repo.findByUsername('qa_test'); console.log('FOUND:' + (found !== null)); const notFound = repo.findByUsername('nonexistent'); console.log('MISSING:' + (notFound === null));"
      2. Check CREATED contains id and username:'qa_test'
      3. Check FOUND:true
      4. Check MISSING:true
    Expected Result: User created with id, findable by username, non-existent returns null
    Evidence: .omo/evidence/task-6-crud.txt

  Scenario: Duplicate username throws
    Tool: Bash (node REPL)
    Steps:
      1. Run: node -e "const repo = require('./src/lib/db/repos/usersRepo'); repo.createUser('dupe_test', 'hash1'); try { repo.createUser('dupe_test', 'hash2'); console.log('NO_ERROR'); } catch(e) { console.log('DUPLICATE:' + e.message.substring(0,30)); }"
    Expected Result: DUPLICATE:... (error message about unique constraint)
    Evidence: .omo/evidence/task-6-duplicate.txt
  ```

  **Evidence**: `.omo/evidence/task-6-crud.txt`, `.omo/evidence/task-6-duplicate.txt`
  **Commit**: YES — `feat(db): add usersRepo with full CRUD operations`
  **Files**: `src/lib/db/repos/usersRepo.js`, `src/lib/db/repos/usersRepo.test.js`

- [ ] 7. Create rateLimitRepo.js — config + enforcement queries

  **What to do**:
  - Create `src/lib/db/repos/rateLimitRepo.js` following existing repo patterns
  - Functions: `createDefaults(userId)` — insert default limits row, `getByUserId(userId)` — get current limits, `updateLimits(userId, limits)` — update partial fields, `getUsageToday(apiKey)` — SUM of usageDaily for today, `getMonthlyTokens(userId)` — join apiKeys → usageHistory to sum tokens for all user's keys this month, `getActiveConcurrent(userId)` — placeholder for in-memory check (query returns the limit config only, actual concurrency counting is in middleware)
  - Write unit test: test defaults insertion, retrieval, update, monthly aggregation

  **Must NOT do**:
  - Don't implement rate limit enforcement logic here (that's middleware, Task 12)
  - Don't implement concurrent counting here (in-memory, done in middleware)

  **Recommended Agent Profile**:
  - **Category**: `quick` — Data access layer, standard repo pattern.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 1-6)
  - **Parallel Group**: Wave 1
  - **Blocks**: Tasks 12 (middleware), 17 (limits API), 24 (integration tests)
  - **Blocked By**: Task 2 (schema), Task 1 (vitest)

  **References**:
  - `src/lib/db/repos/apiKeysRepo.js` — Repo pattern (DB import, function style, error handling)
  - `src/lib/db/repos/usageRepo.js` — Existing usage tracking queries (to join with for token counting)
  - `src/lib/db/schema.js` — rateLimits + users + apiKeys column names

  **Acceptance Criteria**:
  - [ ] `createDefaults(userId)` inserts row with strict defaults (100/min, 1M tokens, $5 credit, 5 concurrent)
  - [ ] `getByUserId(userId)` returns limits object or null
  - [ ] `updateLimits(userId, {rtpMinute: 200})` updates only that field, leaves others
  - [ ] `getMonthlyTokens(userId)` aggregates across user's apiKeys correctly
  - [ ] Unit test passes 4+ tests

  **QA Scenarios**:
  ```
  Scenario: Create defaults and retrieve
    Tool: Bash (node REPL)
    Preconditions: DB synced, user exists (id from usersRepo.createUser)
    Steps:
      1. Create user then set limits: node -e "const ur = require('./src/lib/db/repos/usersRepo'); const rlr = require('./src/lib/db/repos/rateLimitRepo'); const u = ur.createUser('limit_test', 'h'); rlr.createDefaults(u.id); const l = rlr.getByUserId(u.id); console.log(JSON.stringify(l));"
      2. Check output has rtpMinute:100, tokenLimitMonthly:1000000, creditLimit:5, maxConcurrent:5
    Expected Result: All 4 default values present and correct
    Evidence: .omo/evidence/task-7-defaults.txt

  Scenario: Partial update preserves other fields
    Tool: Bash (node REPL)
    Preconditions: Limits row exists for user
    Steps:
      1. Run: node -e "...; rlr.updateLimits(u.id, {rtpMinute: 500}); const l2 = rlr.getByUserId(u.id); console.log('RTP:'+l2.rtpMinute+' TOKEN:'+l2.tokenLimitMonthly);"
      2. Check RTP:500, TOKEN:1000000 (unchanged)
    Expected Result: rtpMinute updated to 500, tokenLimitMonthly stays 1000000
    Evidence: .omo/evidence/task-7-partial-update.txt
  ```

  **Evidence**: `.omo/evidence/task-7-defaults.txt`, `.omo/evidence/task-7-partial-update.txt`
  **Commit**: YES — `feat(db): add rateLimitRepo with config and usage queries`
  **Files**: `src/lib/db/repos/rateLimitRepo.js`, `src/lib/db/repos/rateLimitRepo.test.js`

---

### Wave 2 — Auth System & Middleware (all parallel after Wave 1)

- [ ] 8. Create user registration endpoint + tests

  **What to do**:
  - Create `src/app/api/auth/register/route.js` — POST handler
  - Accept `{ username, password }` in request body
  - Validate: username 3-30 chars alphanumeric, password 8+ chars
  - Hash password via `src/lib/auth/password.js`
  - Call `usersRepo.createUser(username, passwordHash)` — creates with role='user', is_approved=0, is_active=1
  - Return 201 `{ message: "Registration submitted. Awaiting admin approval.", userId }`
  - Reject duplicate username with 409
  - Write test: `src/app/api/auth/register/route.test.js` — successful reg, duplicate username, invalid input, short password
  - Add `src/lib/auth/validation.js` with `validateUsername()` and `validatePassword()` helpers

  **Must NOT do**:
  - Don't auto-approve users (is_approved must be 0)
  - Don't auto-login after registration
  - Don't allow admin role via registration (always 'user')

  **Recommended Agent Profile**:
  - **Category**: `unspecified-high` — Next.js API route with validation, DB calls, error handling.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 9-14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Tasks 15, 21
  - **Blocked By**: Tasks 4 (bcrypt), 5 (JWT), 6 (usersRepo)

  **References**:
  - `src/app/api/keys/route.js` — Next.js API route pattern (export function, request handling, response format)
  - `src/lib/db/repos/usersRepo.js` — `createUser`, `findByUsername` signatures
  - `src/lib/auth/password.js` — `hashPassword` signature
  - `src/lib/auth/validation.js` — Create this: username regex, password min-length

  **Acceptance Criteria**:
  - [ ] POST `/api/auth/register` with valid body → 201 + `is_approved: 0` in DB
  - [ ] POST with duplicate username → 409
  - [ ] POST with username < 3 chars → 400
  - [ ] POST with password < 8 chars → 400
  - [ ] Unit test passes 4+ tests

  **QA Scenarios**:
  ```
  Scenario: Successful registration
    Tool: Bash (curl)
    Preconditions: Server running on port 20128, DB contains no user 'regtest'
    Steps:
      1. curl -X POST http://localhost:20128/api/auth/register -H "Content-Type: application/json" -d '{"username":"regtest","password":"password123"}'
      2. Check status code is 201
      3. Check response body contains "registration" (case insensitive) and "awaiting"
    Expected Result: HTTP 201, body has "registration" and "awaiting" or "approval"
    Evidence: .omo/evidence/task-8-register.txt

  Scenario: Duplicate username rejected
    Tool: Bash (curl)
    Preconditions: 'regtest' already registered from previous scenario
    Steps:
      1. curl -X POST http://localhost:20128/api/auth/register -H "Content-Type: application/json" -d '{"username":"regtest","password":"another123"}'
      2. Check status code is 409
    Expected Result: HTTP 409
    Evidence: .omo/evidence/task-8-duplicate.txt

  Scenario: Invalid password rejected
    Tool: Bash (curl)
    Steps:
      1. curl -X POST http://localhost:20128/api/auth/register -H "Content-Type: application/json" -d '{"username":"shortpw","password":"123"}'
      2. Check status code is 400
    Expected Result: HTTP 400
    Evidence: .omo/evidence/task-8-shortpw.txt
  ```

  **Evidence**: `.omo/evidence/task-8-register.txt`, `.omo/evidence/task-8-duplicate.txt`, `.omo/evidence/task-8-shortpw.txt`
  **Commit**: YES — `feat(auth): add user registration endpoint`
  **Files**: `src/app/api/auth/register/route.js`, `src/lib/auth/validation.js`, test files

- [ ] 9. Create user login endpoint + tests

  **What to do**:
  - Create `src/app/api/auth/login/route.js` — POST handler
  - Accept `{ username, password }` in request body
  - Lookup user via `usersRepo.findByUsername(username)`
  - Check user exists, is_active=1, is_approved=1 — reject with 403 if not approved, 401 if wrong credentials
  - Verify password via `password.verifyPassword(password, user.password_hash)`
  - Sign JWT via `jwt.signToken({ userId: user.id, username: user.username, role: user.role })`
  - Set JWT as httpOnly cookie `auth_token` (same cookie name as existing dashboardGuard expects)
  - Return 200 `{ user: { id, username, role } }`
  - Write test `src/app/api/auth/login/route.test.js`: valid login, wrong password, unapproved user, disabled user

  **Must NOT do**:
  - Don't honor old INITIAL_PASSWORD (that's removed — only bcrypt users now)
  - Don't leak which field is wrong (use generic "Invalid credentials" for both missing user and wrong password)

  **Recommended Agent Profile**:
  - **Category**: `unspecified-high` — Auth endpoint with credential verification, JWT, cookies.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8, 10-14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Tasks 13, 20
  - **Blocked By**: Tasks 4 (bcrypt), 5 (JWT), 6 (usersRepo)

  **References**:
  - `src/dashboardGuard.js` — How `auth_token` cookie is currently read (cookie name, httpOnly, path, SameSite)
  - `src/lib/auth/jwt.js` — `signToken` signature
  - `src/lib/auth/password.js` — `verifyPassword` signature
  - `src/lib/db/repos/usersRepo.js` — `findByUsername` signature

  **Acceptance Criteria**:
  - [ ] Valid login → 200, `auth_token` cookie set, returns user object
  - [ ] Wrong password → 401, generic "Invalid credentials"
  - [ ] Non-existent user → 401, generic "Invalid credentials" (same message)
  - [ ] Existing user, is_approved=0 → 403 "Account pending approval"
  - [ ] is_active=0 user → 403 "Account disabled"
  - [ ] Unit test passes 5+ tests

  **QA Scenarios**:
  ```
  Scenario: Successful login
    Tool: Bash (curl)
    Preconditions: User 'logintest' registered and manually approved in DB (or admin seed creates approved user)
    Steps:
      1. curl -v -X POST http://localhost:20128/api/auth/login -H "Content-Type: application/json" -d '{"username":"logintest","password":"password123"}'
      2. Check status code is 200
      3. Check Set-Cookie header contains 'auth_token'
      4. Check response body contains 'id' and 'username'
    Expected Result: HTTP 200, Set-Cookie with auth_token, body has user object
    Evidence: .omo/evidence/task-9-login.txt

  Scenario: Wrong password returns 401
    Tool: Bash (curl)
    Steps:
      1. curl -X POST http://localhost:20128/api/auth/login -H "Content-Type: application/json" -d '{"username":"logintest","password":"WRONG"}'
      2. Check status code is 401
    Expected Result: HTTP 401
    Evidence: .omo/evidence/task-9-wrongpw.txt

  Scenario: Unapproved user rejected with 403
    Tool: Bash (curl)
    Preconditions: User registered but NOT approved (is_approved=0)
    Steps:
      1. curl -X POST http://localhost:20128/api/auth/login -H "Content-Type: application/json" -d '{"username":"pendinguser","password":"password123"}'
      2. Check status code is 403
    Expected Result: HTTP 403
    Evidence: .omo/evidence/task-9-unapproved.txt
  ```

  **Evidence**: `.omo/evidence/task-9-login.txt`, `.omo/evidence/task-9-wrongpw.txt`, `.omo/evidence/task-9-unapproved.txt`
  **Commit**: YES — `feat(auth): add multi-user login endpoint with JWT`
  **Files**: `src/app/api/auth/login/route.js`, test files

- [ ] 10. Auto-create first admin from INITIAL_PASSWORD

  **What to do**:
  - Create `src/lib/auth/seedAdmin.js` — function called at app startup (or in dashboardGuard init)
  - Check if any user with role='admin' exists in users table
  - If no admin exists AND `process.env.INITIAL_PASSWORD` is set: create admin user with username 'admin', hashed INITIAL_PASSWORD, role='admin', is_approved=1, is_active=1
  - Also call `rateLimitRepo.createDefaults(adminUser.id)` to give admin default limits
  - If INITIAL_PASSWORD not set when no admin exists, log warning (don't crash)
  - If admin already exists, skip (no-op)
  - Write test: first run creates admin, second run is no-op, no INITIAL_PASSWORD → no crash

  **Must NOT do**:
  - Don't overwrite existing admin
  - Don't crash the app if INITIAL_PASSWORD missing (just log warning)
  - Don't create duplicate admins

  **Recommended Agent Profile**:
  - **Category**: `quick` — Simple startup check and conditional insert.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8-9, 11-14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 13 (dashboardGuard calls this on init)
  - **Blocked By**: Tasks 4 (bcrypt), 6 (usersRepo), 7 (rateLimitRepo)

  **References**:
  - `src/lib/db/repos/usersRepo.js` — `findByUsername('admin')`, `createUser`
  - `src/lib/db/repos/rateLimitRepo.js` — `createDefaults`
  - `src/lib/auth/password.js` — `hashPassword`
  - `.env.example` — Check INITIAL_PASSWORD already exists, confirm format

  **Acceptance Criteria**:
  - [ ] First run with INITIAL_PASSWORD → admin user created in DB
  - [ ] Second run → no duplicate, no crash
  - [ ] Admin has role='admin', is_approved=1, is_active=1
  - [ ] Admin gets default rate limits row created
  - [ ] Missing INITIAL_PASSWORD → logged warning, no crash

  **QA Scenarios**:
  ```
  Scenario: First admin created on fresh DB
    Tool: Bash (node REPL)
    Preconditions: Empty/fresh users table, INITIAL_PASSWORD=testadmin123 in env
    Steps:
      1. Run: node -e "process.env.INITIAL_PASSWORD='testadmin123'; require('./src/lib/auth/seedAdmin')(); const repo = require('./src/lib/db/repos/usersRepo'); const a = repo.findByUsername('admin'); console.log('ADMIN:' + (a ? a.role + ':' + a.is_approved : 'NONE'));"
      2. Check ADMIN:admin:1
    Expected Result: Admin user exists with role admin and approved
    Evidence: .omo/evidence/task-10-admin-created.txt

  Scenario: Second run is idempotent
    Tool: Bash (node REPL)
    Preconditions: Admin already exists
    Steps:
      1. Run seedAdmin again, then count: node -e "...; const all = repo.listUsers(); const admins = all.filter(u=>u.role==='admin'); console.log('ADMIN_COUNT:' + admins.length);"
      2. Check ADMIN_COUNT:1
    Expected Result: Exactly 1 admin, no duplicates
    Evidence: .omo/evidence/task-10-idempotent.txt
  ```

  **Evidence**: `.omo/evidence/task-10-admin-created.txt`, `.omo/evidence/task-10-idempotent.txt`
  **Commit**: YES — `feat(auth): auto-create first admin from INITIAL_PASSWORD`
  **Files**: `src/lib/auth/seedAdmin.js`, test file

- [ ] 11. Update apiKeysRepo — add userId FK + ownership

  **What to do**:
  - Open `src/lib/db/repos/apiKeysRepo.js` — read all current functions
  - Modify `createApiKey()` to accept and store `userId` parameter (required)
  - Add `findByUserId(userId)` — get all keys owned by a user
  - Add `findByIdAndUserId(id, userId)` — get specific key, ensuring ownership
  - Modify `deleteApiKey(id)` to optionally check userId ownership (or keep global delete for admins, add `deleteApiKeyForUser(id, userId)` for self-service)
  - Update existing calls to `createApiKey` throughout codebase (search: `createApiKey(` in `src/`) — add userId param or note in plan where changes needed
  - Write test: create with userId, retrieve by userId, ownership check, cascade delete when user deleted

  **Must NOT do**:
  - Don't break existing API key functionality (just add userId field)
  - Don't drop the apiKeys table (alter to add column per Task 2)

  **Recommended Agent Profile**:
  - **Category**: `unspecified-high` — Modify existing repo, find callers, update all references.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8-10, 12-14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Tasks 15, 17, 23
  - **Blocked By**: Tasks 6 (usersRepo — need user IDs to exist)

  **References**:
  - `src/lib/db/repos/apiKeysRepo.js` — Current full implementation
  - `src/lib/db/schema.js` — New userId column in apiKeys
  - Search `src/` for `createApiKey(` — find all callers that need updating
  - `src/sse/services/auth.js` — Key place where API keys are validated (may need updated logic)

  **Acceptance Criteria**:
  - [ ] `createApiKey` requires `userId` parameter
  - [ ] `findByUserId(userId)` returns array of keys
  - [ ] `findByIdAndUserId(id, userId)` returns key or null with ownership guarantee
  - [ ] All existing callers of `createApiKey` updated (grep to verify zero unfixed callers)
  - [ ] Unit test passes 5+ tests

  **QA Scenarios**:
  ```
  Scenario: Create API key with userId
    Tool: Bash (node REPL)
    Preconditions: User exists from usersRepo
    Steps:
      1. Create user, then create key: node -e "const ur=require('./src/lib/db/repos/usersRepo'); const akr=require('./src/lib/db/repos/apiKeysRepo'); const u=ur.createUser('keytest','h'); const k=akr.createApiKey('test-key', u.id, 'test'); console.log('KEY_USERID:'+k.userId+' KEY_ID:'+k.id);"
      2. Check KEY_USERID matches the user id
    Expected Result: API key has userId field matching creator's id
    Evidence: .omo/evidence/task-11-key-create.txt

  Scenario: findByUserId returns user's keys only
    Tool: Bash (node REPL)
    Preconditions: Two users, each with keys
    Steps:
      1. Create user1 with key, user2 with key
      2. Run: akr.findByUserId(user1.id) — check count = 1, not 2
    Expected Result: Only user1's keys returned
    Evidence: .omo/evidence/task-11-ownership.txt
  ```

  **Evidence**: `.omo/evidence/task-11-key-create.txt`, `.omo/evidence/task-11-ownership.txt`
  **Commit**: YES — `feat(db): add userId FK to apiKeys, ownership queries`
  **Files**: `src/lib/db/repos/apiKeysRepo.js`, all caller files

- [ ] 12. Implement rate limit enforcement middleware

  **What to do**:
  - Create `src/lib/auth/rateLimitMiddleware.js`
  - Function: `checkRateLimits(apiKey)` → `{ allowed: boolean, reason?: string }`
  - Reads apiKey record to find userId (via apiKeysRepo)
  - Retrieves rate limits config from rateLimitRepo
  - Checks 4 strategies in order:
    1. **RTPM (requests per minute)**: Count usageDaily records for this apiKey in last 60 seconds → reject if ≥ rtpMinute
    2. **RTPD (requests per day)**: Count usageDaily today → reject if ≥ rtpDay
    3. **Token count**: Sum tokensUsed from usageHistory this month across all user's apiKeys → reject if ≥ tokenLimitMonthly
    4. **Credit/quota**: Sum cost from usageHistory this month across all user's apiKeys → reject if ≥ creditLimit
  - Helper: `incrementConcurrent(userId)` → tracks active request count in in-memory Map, rejects if ≥ maxConcurrent
  - Helper: `decrementConcurrent(userId)` → called after request completes (in response handler)
  - Write unit test `src/lib/auth/rateLimitMiddleware.test.js`: mock repo calls, test each strategy blocked/allowed

  **Must NOT do**:
  - Don't use setTimeout-based windowing (check real-time DB counts on each request)
  - Don't persist concurrent counts to DB (in-memory only, resets on server restart)
  - Don't apply rate limits to admin users (skip check for role='admin')

  **Recommended Agent Profile**:
  - **Category**: `deep` — Multi-strategy rate limiting with DB aggregation queries, in-memory concurrency tracking. Complex logic.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8-11, 13-14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Tasks 13, 24
  - **Blocked By**: Tasks 7 (rateLimitRepo), 11 (apiKeysRepo with userId), 13 (partial — middleware is integrated in dashboardGuard but can be developed standalone)

  **References**:
  - `src/lib/db/repos/rateLimitRepo.js` — `getByUserId`, `getUsageToday`, `getMonthlyTokens`
  - `src/lib/db/repos/apiKeysRepo.js` — `findById` to get userId from apiKey
  - `src/lib/db/repos/usageRepo.js` — `usageHistory` and `usageDaily` table structure for query design
  - `src/dashboardGuard.js` — Where this middleware will be called (in `canAccessPublicLlmApi`)

  **Acceptance Criteria**:
  - [ ] Check with valid apiKey, under all limits → `{ allowed: true }`
  - [ ] Check with exceeded RTPM → `{ allowed: false, reason: "Rate limit exceeded (requests per minute)" }`
  - [ ] Check with exceeded RTPD → blocked with reason
  - [ ] Check with exceeded token limit → blocked with reason
  - [ ] Check with exceeded credit → blocked with reason
  - [ ] Concurrent count: increment → at max → next increment rejects
  - [ ] Admin users always allowed (skip all checks)
  - [ ] Unit test passes 8+ tests

  **QA Scenarios**:
  ```
  Scenario: Request under limits is allowed
    Tool: Bash (node REPL)
    Preconditions: Mock DB with user, apiKey, limits, zero usage
    Steps:
      1. Run checkRateLimits with the test apiKey
      2. Assert allowed: true
    Expected Result: { allowed: true }
    Evidence: .omo/evidence/task-12-allowed.txt

  Scenario: Exceeded concurrent limit blocks
    Tool: Bash (node REPL)
    Preconditions: Set maxConcurrent=2, call incrementConcurrent 2x
    Steps:
      1. Call incrementConcurrent 2x (fills slots)
      2. Call incrementConcurrent a 3rd time
      3. Assert rejected with concurrent message
      4. Call decrementConcurrent once, then incrementConcurrent again
      5. Assert 3rd increment now allowed
    Expected Result: 3rd call rejected, after decrement it passes
    Evidence: .omo/evidence/task-12-concurrent.txt

  Scenario: Admin bypasses all checks
    Tool: Bash (node REPL)
    Preconditions: User with role='admin', exceeded all limits
    Steps:
      1. Call checkRateLimits for admin's apiKey
      2. Assert allowed: true regardless of exceeded limits
    Expected Result: { allowed: true }
    Evidence: .omo/evidence/task-12-admin-bypass.txt
  ```

  **Evidence**: `.omo/evidence/task-12-allowed.txt`, `.omo/evidence/task-12-concurrent.txt`, `.omo/evidence/task-12-admin-bypass.txt`
  **Commit**: YES — `feat(ratelimit): implement 4-strategy rate limit middleware`
  **Files**: `src/lib/auth/rateLimitMiddleware.js`, test file

- [ ] 13. Update dashboardGuard — multi-user auth + rate limit gate

  **What to do**:
  - Open `src/dashboardGuard.js` — read full file, understand current middleware chain
  - Replace old INITIAL_PASSWORD single-password check with JWT verification:
    - Read `auth_token` cookie
    - Call `jwt.verifyToken(token)` → extract `{ userId, username, role }`
    - If no token/invalid → redirect to `/login`
    - Set `req.user` = `{ userId, username, role }` for downstream handlers
  - Add `seedAdmin()` call at first middleware invocation (lazy init)
  - In the public LLM API gate function (look for `canAccessPublicLlmApi` or similar LLM proxy guard):
    - Extract apiKey from request (or from req query params, depends on how existing code works)
    - Call `rateLimitMiddleware.checkRateLimits(apiKey)` + `rateLimitMiddleware.incrementConcurrent(userId)`
    - If blocked → return 429 with specific retry-after header
    - After response completes, call `decrementConcurrent(userId)` (need to hook into response close/finish)
  - Write test `src/dashboardGuard.test.js`: test JWT valid/invalid, test rate limit rejection, test admin access

  **Must NOT do**:
  - Don't remove any existing non-auth middleware (static file serving, CORS, etc.)
  - Don't break the LLM proxy functionality — only add gate at the right point
  - Don't cache JWT result across requests (decode each time for simplicity)

  **Recommended Agent Profile**:
  - **Category**: `deep` — Critical security middleware. Must understand full existing chain, add auth + rate limiting without breaking proxy.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8-12, 14)
  - **Parallel Group**: Wave 2
  - **Blocks**: Tasks 15-17, 24-25
  - **Blocked By**: Tasks 5 (JWT), 9 (login), 10 (seed admin), 12 (rate limit middleware)

  **References**:
  - `src/dashboardGuard.js` — Full current implementation (read completely)
  - `src/lib/auth/jwt.js` — `verifyToken` signature
  - `src/lib/auth/rateLimitMiddleware.js` — `checkRateLimits`, `incrementConcurrent`, `decrementConcurrent`
  - `src/lib/auth/seedAdmin.js` — Seed function to call on init
  - `src/sse/services/auth.js` — How SSE path validates API keys (may need same rate limit check)

  **Acceptance Criteria**:
  - [ ] Valid JWT in auth_token cookie → allowed access, req.user populated
  - [ ] Invalid/missing JWT → redirected to /login
  - [ ] Expired JWT → redirected to /login
  - [ ] Rate limit exceeded for user's apiKey → 429 with JSON body and Retry-After header
  - [ ] Admin role can access /dashboard/* routes
  - [ ] Old INITIAL_PASSWORD auth completely removed (grep for INITIAL_PASSWORD in dashboardGuard → zero uses)
  - [ ] First admin auto-created on first request

  **QA Scenarios**:
  ```
  Scenario: Valid JWT grants access
    Tool: Bash (curl)
    Preconditions: Server running, user logged in (auth_token cookie)
    Steps:
      1. Login to get cookie: curl -c cookies.txt -X POST http://localhost:20128/api/auth/login -d '{"username":"admin","password":"testadmin123"}'
      2. Access dashboard: curl -b cookies.txt http://localhost:20128/dashboard
      3. Check status code is 200 (not 302/redirect)
    Expected Result: HTTP 200, not redirected
    Evidence: .omo/evidence/task-13-jwt-access.txt

  Scenario: No token redirects to login
    Tool: Bash (curl)
    Steps:
      1. curl -v http://localhost:20128/dashboard 2>&1
      2. Check status is 302 and Location header contains /login
    Expected Result: HTTP 302, Location contains /login
    Evidence: .omo/evidence/task-13-redirect.txt

  Scenario: Rate limit exceeded returns 429
    Tool: Bash (curl)
    Preconditions: Set user's RTPM to 1, make 2 rapid requests
    Steps:
      1. curl -b cookies.txt http://localhost:20128/api/some-endpoint
      2. curl -b cookies.txt http://localhost:20128/api/some-endpoint
      3. Second request status is 429
    Expected Result: Second request HTTP 429
    Evidence: .omo/evidence/task-13-ratelimit.txt
  ```

  **Evidence**: `.omo/evidence/task-13-jwt-access.txt`, `.omo/evidence/task-13-redirect.txt`, `.omo/evidence/task-13-ratelimit.txt`
  **Commit**: YES — `feat(auth): replace single-password with JWT multi-user auth + rate limit gate`
  **Files**: `src/dashboardGuard.js`, test file

- [ ] 14. Auth status endpoint

  **What to do**:
  - Create `src/app/api/auth/me/route.js` — GET handler
  - Read `auth_token` cookie, verify JWT
  - If valid: return 200 `{ user: { id, username, role }, authenticated: true }`
  - If invalid/expired: return 200 `{ authenticated: false }` (not 401 — this is a status check)
  - Write test: valid returns user, invalid returns false, no cookie returns false

  **Must NOT do**:
  - Don't return 401 (this is a status check, not a protected resource)
  - Don't expose password_hash or other sensitive fields

  **Recommended Agent Profile**:
  - **Category**: `quick` — Simple GET endpoint, read cookie, verify JWT, return status.
  - **Skills**: None needed.

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 8-13)
  - **Parallel Group**: Wave 2
  - **Blocks**: None directly (utility endpoint)
  - **Blocked By**: Tasks 5 (JWT), 6 (usersRepo)

  **References**:
  - `src/dashboardGuard.js` — Cookie name (`auth_token`), JWT verification pattern
  - `src/lib/auth/jwt.js` — `verifyToken` signature
  - `src/app/api/auth/login/route.js` — Response format pattern (user object shape)

  **Acceptance Criteria**:
  - [ ] Valid cookie → 200 `{ authenticated: true, user: { id, username, role } }`
  - [ ] Invalid cookie → 200 `{ authenticated: false }`
  - [ ] No cookie → 200 `{ authenticated: false }`

  **QA Scenarios**:
  ```
  Scenario: Authenticated user gets user object
    Tool: Bash (curl)
    Preconditions: Logged in, holding valid auth_token
    Steps:
      1. Login then: curl -b cookies.txt http://localhost:20128/api/auth/me
      2. Check status 200, body has "authenticated":true and user object with id
    Expected Result: HTTP 200, authenticated: true, user present
    Evidence: .omo/evidence/task-14-authenticated.txt

  Scenario: No cookie returns false
    Tool: Bash (curl)
    Steps:
      1. curl http://localhost:20128/api/auth/me
      2. Check status 200, body has "authenticated":false
    Expected Result: HTTP 200, authenticated: false
    Evidence: .omo/evidence/task-14-unauthenticated.txt
  ```

  **Evidence**: `.omo/evidence/task-14-authenticated.txt`, `.omo/evidence/task-14-unauthenticated.txt`
  **Commit**: YES — `feat(auth): add /api/auth/me status endpoint`
  **Files**: `src/app/api/auth/me/route.js`, test file

---

### Wave 3 — Admin API & Dashboard UI

- [ ] 15. Admin users CRUD API endpoint

  **What to do**:
  - Create `src/app/api/users/route.js` — GET (list all users, admin-only)
  - Create `src/app/api/users/[id]/route.js` — GET (single), PATCH (update role/is_active), DELETE (cascade)
  - All endpoints: verify `req.user.role === 'admin'` → reject non-admin with 403
  - GET list: return `[{ id, username, role, is_active, is_approved, created_at }]` (no password_hash)
  - PATCH: allow `role` and `is_active` only, reject other fields
  - DELETE: remove user, cascade apiKeys, cascade rateLimits
  - Guard: cannot demote/delete last admin → 400, cannot self-delete → 400

  **Must NOT do**: Expose password_hash. Allow non-admin access.

  **Recommended Agent Profile**: `unspecified-high` — Admin CRUD with auth, cascade, guards.
  **Parallelization**: Wave 3 (with 16-21) | Blocks 18,19 | Blocked by 6,11,13

  **References**: `src/app/api/keys/route.js` (route pattern), `src/lib/db/repos/usersRepo.js`, `src/lib/db/repos/apiKeysRepo.js`, `src/dashboardGuard.js`

  **Acceptance Criteria**:
  - [ ] GET /api/users as admin → 200, array, no password_hash
  - [ ] GET /api/users as user → 403
  - [ ] PATCH role → 200; cannot demote last admin → 400
  - [ ] DELETE → 200, cascade verified

  **QA Scenarios**:
  ```
  Scenario: Admin lists users, no password_hash leaked
    Tool: Bash (curl)
    Preconditions: Admin logged in, 2+ users in DB
    Steps:
      1. curl -b cookies.txt http://localhost:20128/api/users
      2. Assert status 200, JSON array with 2+ entries
      3. Assert no entry has "password_hash" key
    Expected Result: HTTP 200, array, no password_hash present
    Evidence: .omo/evidence/task-15-list.txt

  Scenario: Non-admin gets 403
    Tool: Bash (curl)
    Preconditions: Regular user logged in
    Steps: curl -b user_cookies.txt http://localhost:20128/api/users → assert 403
    Expected Result: HTTP 403
    Evidence: .omo/evidence/task-15-forbidden.txt
  ```

  **Evidence**: `.omo/evidence/task-15-list.txt`, `.omo/evidence/task-15-forbidden.txt`
  **Commit**: YES — `feat(api): add admin users CRUD endpoints`
  **Files**: `src/app/api/users/route.js`, `src/app/api/users/[id]/route.js`, tests

- [ ] 16. Admin user approval API

  **What to do**:
  - Create `src/app/api/users/[id]/approve/route.js` — POST (approve), DELETE (revoke)
  - POST: `usersRepo.updateApproval(id, true)` → 200
  - DELETE: `usersRepo.updateApproval(id, false)` → 200
  - Cannot self-approve → 400

  **Must NOT do**: Auto-login after approval.

  **Recommended Agent Profile**: `quick` — Toggle endpoint.
  **Parallelization**: Wave 3 | Blocks 18,19 | Blocked by 6,13,15

  **References**: `src/app/api/users/[id]/route.js` (param pattern), `src/lib/db/repos/usersRepo.js`

  **Acceptance Criteria**:
  - [ ] POST approve → user.is_approved = 1
  - [ ] DELETE revoke → user.is_approved = 0
  - [ ] Self-approve → 400

  **QA Scenarios**:
  ```
  Scenario: Approve then login
    Tool: Bash (curl)
    Preconditions: Pending user, admin logged in
    Steps:
      1. curl -X POST -b cookies.txt http://localhost:20128/api/users/[id]/approve → assert 200
      2. Login as approved user → assert 200 (not 403)
    Expected Result: 200 on approve, then 200 on login
    Evidence: .omo/evidence/task-16-approve-login.txt
  ```

  **Evidence**: `.omo/evidence/task-16-approve-login.txt`
  **Commit**: YES — `feat(api): add admin user approval endpoint`
  **Files**: `src/app/api/users/[id]/approve/route.js`, test

- [ ] 17. Admin rate limits config API

  **What to do**:
  - Create `src/app/api/users/[id]/limits/route.js` — GET + PATCH
  - GET: return limits from `rateLimitRepo.getByUserId(id)`
  - PATCH: partial update via `rateLimitRepo.updateLimits(id, body)`
  - Validate: values ≥ 1 (creditLimit ≥ 0), reject negative

  **Must NOT do**: Let users modify own limits.

  **Recommended Agent Profile**: `unspecified-high` — Config API with validation.
  **Parallelization**: Wave 3 | Blocks 19 | Blocked by 7,13,15

  **References**: `src/lib/db/repos/rateLimitRepo.js` (getByUserId, updateLimits)

  **Acceptance Criteria**:
  - [ ] GET returns full limits object with defaults
  - [ ] PATCH `{rtpMinute: 200}` → only that field changes, others preserved
  - [ ] PATCH negative → 400

  **QA Scenarios**:
  ```
  Scenario: Get and update limits
    Tool: Bash (curl)
    Preconditions: Admin logged in
    Steps:
      1. GET limits → assert rtpMinute:100
      2. PATCH {"rtpMinute":500} → assert 200
      3. GET again → assert rtpMinute:500, creditLimit still 5
    Expected Result: Partial update works, other fields unchanged
    Evidence: .omo/evidence/task-17-update.txt
  ```

  **Evidence**: `.omo/evidence/task-17-update.txt`
  **Commit**: YES — `feat(api): add admin rate limits config endpoint`
  **Files**: `src/app/api/users/[id]/limits/route.js`, test

- [ ] 18. Admin users dashboard page

  **What to do**:
  - Create `src/app/(dashboard)/dashboard/users/page.js` — Next.js page
  - Fetch users from `/api/users`, display table: Username, Role, Status (approved/pending/disabled), Created, Actions
  - Actions: Approve button (for pending), Edit link, Delete button (confirm dialog)
  - Use existing Tailwind patterns (match other dashboard pages: sidebar, cards, tables)
  - States: loading (skeleton/spinner), empty ("No users yet"), error (retry button)

  **Must NOT do**: Install component libraries. Expose password_hash.

  **Recommended Agent Profile**: `visual-engineering` — Table UI with actions, states.
  **Parallelization**: Wave 3 (with 15-17,19-21) | Blocks 22 | Blocked by 13,15

  **References**: `src/app/(dashboard)/dashboard/keys/page.js` (page pattern), `src/shared/components/Sidebar.js`, `src/app/(dashboard)/dashboard/layout.js`

  **Acceptance Criteria**:
  - [ ] Table renders with all columns + action buttons
  - [ ] Approve button visible for pending users
  - [ ] Loading, empty, error states handle gracefully

  **QA Scenarios**:
  ```
  Scenario: Admin sees user list with correct roles
    Tool: Playwright
    Preconditions: Admin logged in, 3 users exist (admin + 2 regular)
    Steps:
      1. Navigate to http://localhost:20128/dashboard/users
      2. Wait for [data-testid="users-table"] to render
      3. Assert page shows all 3 usernames
      4. Assert admin role badge visible for admin user
    Expected Result: Full user table with correct roles displayed
    Evidence: .omo/evidence/task-18-list.png
  ```

  **Evidence**: `.omo/evidence/task-18-list.png`
  **Commit**: YES — `feat(ui): add admin users dashboard page`
  **Files**: `src/app/(dashboard)/dashboard/users/page.js`

- [ ] 19. Admin user detail + limits page

  **What to do**:
  - Create `src/app/(dashboard)/dashboard/users/[id]/page.js`
  - Fetch user from `/api/users/[id]` + limits from `/api/users/[id]/limits`
  - Display: user info card (username, role badge, status, created date)
  - Limits form: 5 input fields pre-filled (rtpMinute, rtpDay, tokenLimitMonthly, creditLimit, maxConcurrent)
  - Save button → PATCH `/api/users/[id]/limits`
  - Show user's API keys (fetch with userId filter)
  - Role/Active toggles → PATCH `/api/users/[id]`
  - Back link to users list

  **Must NOT do**: Show password_hash. Allow deleting last admin.

  **Recommended Agent Profile**: `visual-engineering` — Detail form with inputs, toggles.
  **Parallelization**: Wave 3 | Blocks 22 | Blocked by 13,15,17

  **References**: `src/app/(dashboard)/dashboard/keys/[id]/page.js` (detail pattern), Tailwind form patterns from existing pages

  **Acceptance Criteria**:
  - [ ] User info card renders with all fields
  - [ ] Limits form pre-fills from API response
  - [ ] Save updates limit fields and shows success feedback
  - [ ] API keys list for this user displayed

  **QA Scenarios**:
  ```
  Scenario: Edit user limits via form
    Tool: Playwright
    Preconditions: Admin on user detail page
    Steps:
      1. Navigate to http://localhost:20128/dashboard/users/[id]
      2. Change rtpMinute input to 500
      3. Click Save
      4. Assert success toast/message appears
      5. Reload page, assert rtpMinute still 500
    Expected Result: Limits persist after save and reload
    Evidence: .omo/evidence/task-19-limits.png
  ```

  **Evidence**: `.omo/evidence/task-19-limits.png`
  **Commit**: YES — `feat(ui): add admin user detail + limits config page`
  **Files**: `src/app/(dashboard)/dashboard/users/[id]/page.js`

- [ ] 20. Update login page for multi-user

  **What to do**:
  - Open existing `src/app/login/page.js` (find and read it)
  - Replace any old single-password flow with username + password form
  - POST to `/api/auth/login`, handle 200 → redirect to /dashboard, handle 401/403 → show error
  - Add "Register" link navigating to `/register`
  - Style with existing Tailwind patterns
  - States: idle, loading (disable button + spinner), error (red message), success (redirect)

  **Must NOT do**: Keep any INITIAL_PASSWORD logic on the frontend.

  **Recommended Agent Profile**: `visual-engineering` — Login form with auth flow, states.
  **Parallelization**: Wave 3 (with 15-19,21) | Blocks 22 | Blocked by 9

  **References**: `src/app/login/page.js` (current page), `src/app/api/auth/login/route.js`, existing dashboard design tokens

  **Acceptance Criteria**:
  - [ ] Username + password fields rendered
  - [ ] Valid login → redirect to /dashboard
  - [ ] Invalid login → error message displayed
  - [ ] Register link navigates to /register
  - [ ] Loading state disables form

  **QA Scenarios**:
  ```
  Scenario: Login with correct credentials
    Tool: Playwright
    Preconditions: Approved user exists
    Steps:
      1. Navigate to http://localhost:20128/login
      2. Fill username input with "testuser"
      3. Fill password input with "password123"
      4. Click login button
      5. Assert URL is /dashboard (redirect)
    Expected Result: Redirect to dashboard after successful login
    Evidence: .omo/evidence/task-20-login-redirect.png

  Scenario: Login with wrong password shows error
    Tool: Playwright
    Steps:
      1. Fill username "testuser", password "WRONG", click login
      2. Assert error message visible (red text with "Invalid credentials")
    Expected Result: Error shown, still on /login page
    Evidence: .omo/evidence/task-20-login-error.png
  ```

  **Evidence**: `.omo/evidence/task-20-login-redirect.png`, `.omo/evidence/task-20-login-error.png`
  **Commit**: YES — `feat(ui): update login page for multi-user auth`
  **Files**: `src/app/login/page.js`

- [ ] 21. Registration page

  **What to do**:
  - Create `src/app/register/page.js`
  - Form: username (3-30 chars), password (8+ chars), confirm password
  - POST to `/api/auth/register`, handle 201 → success message + link to login, handle 409 → "Username taken", handle 400 → validation errors
  - Style matching login page
  - Link back to login

  **Must NOT do**: Redirect to dashboard after register (must show "awaiting approval" message).

  **Recommended Agent Profile**: `visual-engineering` — Registration form, validation, success state.
  **Parallelization**: Wave 3 | Blocks 22 | Blocked by 8

  **References**: `src/app/login/page.js` (style match), `src/app/api/auth/register/route.js`

  **Acceptance Criteria**:
  - [ ] Username + password + confirm fields rendered
  - [ ] Valid registration → success message shown
  - [ ] Duplicate username → "Username already taken"
  - [ ] Short password → validation error

  **QA Scenarios**:
  ```
  Scenario: Register new user
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:20128/register
      2. Fill username: "newuser123"
      3. Fill password: "securepass123"
      4. Fill confirm password: "securepass123"
      5. Click register button
      6. Assert success message visible: "Awaiting admin approval"
      7. Assert NOT redirected to dashboard
    Expected Result: Success message, no redirect
    Evidence: .omo/evidence/task-21-register-success.png

  Scenario: Password mismatch blocked
    Tool: Playwright
    Steps:
      1. Fill username, password: "abc12345", confirm: "different"
      2. Click register
      3. Assert error visible: "Passwords do not match"
    Expected Result: Error displayed, form not submitted
    Evidence: .omo/evidence/task-21-password-mismatch.png
  ```

  **Evidence**: `.omo/evidence/task-21-register-success.png`, `.omo/evidence/task-21-password-mismatch.png`
  **Commit**: YES — `feat(ui): add user registration page`
  **Files**: `src/app/register/page.js`

---

### Wave 4 — Integration & Polish

- [ ] 22. Update Sidebar navigation + add new routes

  **What to do**:
  - Open `src/shared/components/Sidebar.js` — read current nav items
  - Add "Users" nav item linking to `/dashboard/users`, with admin-only visibility (check `req.user.role`)
  - If Sidebar is server-rendered, pass user role from the request. If client-rendered, fetch `/api/auth/me`.
  - Verify "Login" link points to `/login` (not old path)
  - Ensure all new routes are accessible: `/login`, `/register`, `/dashboard/users`, `/dashboard/users/[id]`

  **Must NOT do**: Show "Users" link to non-admin roles.

  **Recommended Agent Profile**: `quick` — Navigation update, simple visibility logic.
  **Parallelization**: Wave 4 (with 23-25) | Blocks 25 | Blocked by 18-21

  **References**: `src/shared/components/Sidebar.js` (full current nav), `src/dashboardGuard.js` (how user role is available to components)

  **Acceptance Criteria**:
  - [ ] "Users" nav item visible when logged in as admin
  - [ ] "Users" nav item NOT visible for regular user
  - [ ] All routes navigable without 404

  **QA Scenarios**:
  ```
  Scenario: Admin sees Users in sidebar
    Tool: Playwright
    Preconditions: Admin logged in, on /dashboard
    Steps:
      1. Assert sidebar contains "Users" link
      2. Click "Users" → assert navigated to /dashboard/users
    Expected Result: Users link present, navigation works
    Evidence: .omo/evidence/task-22-sidebar-admin.png

  Scenario: Regular user does NOT see Users link
    Tool: Playwright
    Preconditions: Regular user logged in
    Steps:
      1. Navigate to /dashboard
      2. Assert sidebar does NOT contain "Users" text
    Expected Result: No Users link in sidebar
    Evidence: .omo/evidence/task-22-sidebar-user.png
  ```

  **Evidence**: `.omo/evidence/task-22-sidebar-admin.png`, `.omo/evidence/task-22-sidebar-user.png`
  **Commit**: YES — `feat(ui): add Users nav item to sidebar, admin-only`
  **Files**: `src/shared/components/Sidebar.js`

- [ ] 23. User self-service: view own API keys + usage

  **What to do**:
  - Open existing API keys dashboard page (likely `src/app/(dashboard)/dashboard/keys/page.js`)
  - When a regular user visits, show only THEIR apiKeys (filtered by userId from `req.user`)
  - Admin still sees ALL keys (existing behavior)
  - Optionally add usage stats: show total tokens this month, requests today from `usageRepo` / `rateLimitRepo`
  - Ensure deletion only works on own keys for regular users

  **Must NOT do**: Let regular users see other users' keys.

  **Recommended Agent Profile**: `unspecified-high` — Modify existing pages with auth scoping, usage aggregation.
  **Parallelization**: Wave 4 | Blocks 25 | Blocked by 11,13

  **References**: `src/app/(dashboard)/dashboard/keys/page.js` (current keys page), `src/lib/db/repos/apiKeysRepo.js` (findByUserId), `src/lib/db/repos/usageRepo.js`, `src/dashboardGuard.js` (req.user)

  **Acceptance Criteria**:
  - [ ] Regular user sees only their own API keys
  - [ ] Admin sees all API keys
  - [ ] Usage stats displayed per user (tokens, requests)

  **QA Scenarios**:
  ```
  Scenario: User sees only own keys
    Tool: Playwright
    Preconditions: Two users, each with 1 API key. Login as user1.
    Steps:
      1. Navigate to /dashboard/keys
      2. Assert exactly 1 key visible
      3. Assert key name matches user1's key
    Expected Result: Only user1's key displayed
    Evidence: .omo/evidence/task-23-user-keys.png
  ```

  **Evidence**: `.omo/evidence/task-23-user-keys.png`
  **Commit**: YES — `feat(auth): scope API keys to owning user, add usage display`
  **Files**: `src/app/(dashboard)/dashboard/keys/page.js`, related files

- [ ] 24. Rate limit integration test — all 4 strategies

  **What to do**:
  - Create `src/__tests__/integration/rate-limit.test.js`
  - Set up test: create user with apiKey, set low limits (rtpMinute=2, tokenLimitMonthly=100, creditLimit=0.01, maxConcurrent=1)
  - Test RTPM: make 2 requests (allowed), 3rd rejects → 429
  - Test RTPD: configure rtpDay=2, make 3 → 3rd rejects
  - Test token count: pre-insert usageHistory exceeding tokenLimitMonthly → request rejects 429
  - Test credit: pre-insert usageHistory exceeding creditLimit → request rejects
  - Test concurrent: send 2 overlapping requests (maxConcurrent=1) → 2nd rejects
  - Test admin bypass: admin never gets rate limited
  - Clean up test data after each test

  **Must NOT do**: Run against production DB — use test DB path.

  **Recommended Agent Profile**: `deep` — Complex integration test with all 4 strategies, concurrent simulation, DB pre-seeding.
  **Parallelization**: Wave 4 (with 22,23,25) | Blocks 25 | Blocked by 12,13

  **References**: `src/lib/auth/rateLimitMiddleware.js` (checkRateLimits API), `src/lib/db/repos/` (all repos for test setup)

  **Acceptance Criteria**:
  - [ ] RTPM test: 3rd request returns 429
  - [ ] Token test: over-limit returns 429
  - [ ] Credit test: over-limit returns 429
  - [ ] Concurrent test: 2nd concurrent request returns 429
  - [ ] Admin bypass: admin never gets 429
  - [ ] All tests pass with `vitest run`

  **QA Scenarios**:
  ```
  Scenario: Full rate limit integration suite
    Tool: Bash (vitest)
    Preconditions: Test DB ready, server not needed (unit-level middleware tests)
    Steps:
      1. Run: npm test -- integration/rate-limit.test.js
      2. Assert: 5+ tests pass, 0 failures
    Expected Result: All rate limit strategies tested and passing
    Evidence: .omo/evidence/task-24-rate-limit-tests.txt
  ```

  **Evidence**: `.omo/evidence/task-24-rate-limit-tests.txt`
  **Commit**: YES — `test(ratelimit): integration tests for all 4 strategies`
  **Files**: `src/__tests__/integration/rate-limit.test.js`

- [ ] 25. End-to-end flow test

  **What to do**:
  - Create end-to-end test covering the complete flow:
    1. Seed admin from INITIAL_PASSWORD
    2. Register new user via `/api/auth/register`
    3. Verify user.is_approved = 0 (cannot login)
    4. Admin approves user
    5. Verify user can now login
    6. User creates API key
    7. Admin sets rate limits for user
    8. Verify rate limit enforced (exceed, get 429)
    9. Admin views user list, sees the user
    10. User accesses own keys page, sees only their key
  - Use test DB path for isolation

  **Must NOT do**: Use real INITIAL_PASSWORD from .env — inject in test.

  **Recommended Agent Profile**: `deep` — Full end-to-end orchestration across auth, users, api keys, rate limiting.
  **Parallelization**: Wave 4 (with 22-24) | Blocks F1-F4 | Blocked by 22-24

  **References**: All test patterns from Tasks 1-24

  **Acceptance Criteria**:
  - [ ] Complete flow passes (register → approve → login → create key → set limits → check enforcement)
  - [ ] All intermediate state assertions confirmed
  - [ ] Test runs in vitest and passes

  **QA Scenarios**:
  ```
  Scenario: Complete user lifecycle
    Tool: Bash (vitest)
    Steps:
      1. Run: npm test -- integration/e2e.test.js
      2. Assert all flow steps pass
    Expected Result: Full E2E test passes with 0 failures
    Evidence: .omo/evidence/task-25-e2e.txt
  ```

  **Evidence**: `.omo/evidence/task-25-e2e.txt`
  **Commit**: YES — `test(e2e): complete user registration to rate limit enforcement flow`
  **Files**: `src/__tests__/integration/e2e.test.js`

---

## Final Verification Wave (MANDATORY — after ALL implementation tasks)

> 4 review agents run in PARALLEL. ALL must APPROVE. Present results to user, wait for explicit "okay".

- [ ] F1. **Plan Compliance Audit** — `oracle`
  Read plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search for forbidden patterns. Check evidence files exist in `.omo/evidence/`. Compare deliverables against plan.
  Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`

- [ ] F2. **Code Quality Review** — `unspecified-high`
  Run lint + `vitest run`. Review all changed files for: `any` types, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names.
  Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | VERDICT`

- [ ] F3. **Real Manual QA** — `unspecified-high` (+ playwright if UI)
  Start from clean state. Execute EVERY QA scenario from EVERY task (25 tasks). Test cross-task integration. Test edge cases: empty state, invalid input, concurrent. Save evidence to `.omo/evidence/final-qa/`.
  Output: `Scenarios [N/N pass] | Integration [N/N] | VERDICT`

- [ ] F4. **Scope Fidelity Check** — `deep`
  For each task: read "What to do", read actual diff. Verify 1:1 — everything in plan built, nothing beyond plan. Check "Must NOT do" compliance. Flag unaccounted changes.
  Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`

---

## Commit Strategy

| Wave | Commits | Strategy |
|------|---------|----------|
| 1 | 7 commits | Per-task: `chore:`, `feat(db):`, `feat(auth):` |
| 2 | 7 commits | Per-task: `feat(auth):`, `feat(db):`, `feat(ratelimit):` |
| 3 | 7 commits | Per-task: `feat(api):`, `feat(ui):` |
| 4 | 4 commits | Per-task: `feat(ui):`, `feat(auth):`, `test(ratelimit):`, `test(e2e):` |
| FINAL | 4 commits | Per-task: `chore(review):` |

**Pre-commit for all**: `npm run test` must pass. Files listed in each task's "Files" section.

---

## Success Criteria

### Verification Commands
```bash
# Run all tests
npm run test

# Expected: 25+ test files, 100+ tests, 0 failures

# Check all evidence captured
ls .omo/evidence/
# Expected: 30+ evidence files from QA scenarios

# Start dev server and verify endpoints
curl http://localhost:20128/api/auth/me
# Expected: HTTP 200, {"authenticated": false}
```

### Final Checklist
- [ ] All "Must Have" present (7 items)
- [ ] All "Must NOT Have" absent (7 guardrails)
- [ ] All 25 tasks completed
- [ ] All evidence files captured
- [ ] `vitest run` passes 100%
- [ ] Admin UI functional (users, limits, approval)
- [ ] Self-registration flow works
- [ ] Rate limiting enforced (all 4 strategies)
- [ ] Final verification wave approved
