# 2026-07-16 — P0 security (WT-A)

Closes the deployment-readiness P0s for auth, CORS, body size, hygiene, and client-side secret handling.

## Files changed

**New:**
- `server/src/config/index.js` — single source of truth for env-derived config. Crashes on missing required vars in production.
- `server/scripts/lint-no-backups.js` — `npm run lint:hygiene` fails if any `*.bak` / `*.orig` / `*~` is staged.
- `server/scripts/refactor-jwt-secret.js` — one-shot script that rewrote 56 files.
- `server/scripts/smoke-test.js` — post-change smoke test (health, login, offline secret, me/credentials, 2MB body rejection).
- `server/.env.example` — documents all env vars with required-in-prod set.
- `.harness/changelogs/2026-07-16-p0-security.md` — this file.

**Modified (56 server files):**
- All controllers (54) + `server/src/index.js` + `server/src/services/wsHub.js` no longer hardcode `'africa-alert-secret-key-2024'`. They import `const { jwtSecret: JWT_SECRET } = require('../config')` (or `'./config'` for `index.js`).
- `server/src/index.js`: CORS restricted to `ALLOWED_ORIGINS` in prod (permissive in dev). JSON body limit dropped to 1mb. Added `/api/health` endpoint + 413 error handler for `entity.too.large`.
- `client/src/lib/offlineAuth.ts`: `OFFLINE_JWT_SECRET` now sourced from `localStorage` (populated at login from the new `offline_jwt_secret` response field). Dev fallback only in `import.meta.env.DEV`; null in prod unless `ALLOW_CLIENT_OFFLINE_MINT=true`.
- `client/src/store/auth.ts`: captures `offline_jwt_secret` from `/api/auth/login` response and caches it via `cacheOfflineJwtSecret`.
- `server/package.json`: new scripts `lint:hygiene` and `refactor:jwt-secret`.
- `README.md`: production security checklist section.

**Deleted:**
- `server/src/controllers/sync.controller.js.bak` (6045 bytes).

## Verification

Smoke test (`node server/scripts/smoke-test.js`):
- ✓ `GET /api/health` → 200
- ✓ `POST /api/auth/login` returns `token` + `offline_jwt_secret`
- ✓ `GET /api/auth/me/credentials` (with token) → 200
- ✓ 2MB JSON body → 413 (was 500 before the error handler)
- 5/5 checks pass

Regression net: 14/14 Playwright E2E specs green (`cd client && npx playwright test`).

Hygiene: `npm run lint:hygiene` → "OK — no .bak / .orig / ~ files in server/src or client/src".

`Select-String -Path 'server\src' -Pattern "'africa-alert-secret-key-2024'"` → 0 matches.
`Select-String -Path 'client\src' -Pattern "OFFLINE_JWT_SECRET\s*=\s*'africa-alert"` → 0 matches.

## Known follow-ups (deferred to WT-B, WT-C)

- `WT-B` (offline cache) is independent and should land before the next deploy.
- `WT-C` (docker) will add `HEALTHCHECK` that hits `/api/health` (already present now).
- The dev fallback string `dev-only-insecure-secret-do-not-use-in-production` is intentional; the prod crash on missing `JWT_SECRET` is in `server/src/config/index.js`.
