Adds defence-in-depth shared-secret auth that activates when API_KEY
is set. Behaviour:
- empty API_KEY (dev default): every request allowed, middleware is
not even installed;
- non-empty API_KEY: every request under APP_API_PREFIX except
/health must carry X-API-Key: <value> or
Authorization: Bearer <value>. /, /docs, /redoc, /openapi.json and
CORS preflight stay open. hmac.compare_digest is used for the
constant-time comparison.
The middleware resolves settings lazily so test fixtures can reload
app.config and have the new API_KEY take effect on the next install.
Tests (tests/test_api_security.py, 5 cases):
- /health remains open;
- protected route rejects missing key (401);
- protected route accepts X-API-Key header;
- protected route accepts Authorization: Bearer header;
- protected route rejects a wrong key.
Frontend:
- VITE_API_KEY env reads the key and Axios injects it on every
request, falling back to no header when empty so SSO/reverse-proxy
deployments stay unchanged.
- vite-env.d.ts adds the new env entry.
Docs/ops:
- .env.example documents the dev-default empty key;
- .env.prod.example marks API_KEY as a required rotation point;
- docker-compose.yml forwards API_KEY (defaults to empty);
- docker-compose.prod.yml fails the stack with ?:required when API_KEY
is missing;
- RUNBOOK gains an API authentication section with header examples
and the reverse-proxy + key layering recommendation.
pytest -q: 33 passed (5 new security + 28 prior).
npx tsc --noEmit: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/indexing/qdrant_client.py: remove the identity-only _qid()
helper and pass chunk_id straight to PointStruct (Qdrant accepts
the UUID string directly).
- services/types.ts: SearchHit gets an explicit, optional
ocr_confidence field so consumers can type the value instead of
casting through metadata.
- widgets/SearchResultCard.tsx: replaces the
(hit.metadata as { ocr_confidence? }) cast with the new field. No
behavior change when the backend omits it.
tsc --noEmit: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- vite-env.d.ts now declares ImportMetaEnv with the three VITE_*
variables the project uses, restoring proper typing for
import.meta.env in apiClient.ts.
- QualityFlag.tsx widens its 'flags' prop to accept the domain
QualityFlags type, the loose Record form used in mocks, or null,
ending the structural-mismatch errors at five callsites
(DocumentsPage, DocumentViewerPage, QualityControlPage,
ChunkPreview, SearchResultCard).
- DashboardPage trend callbacks are typed against DashboardStats so
the implicit-any complaints disappear without weakening intent.
npx tsc --noEmit -> clean. vite build -> ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two-job CI (backend + frontend) running ruff, pytest (unit only -
skipping heavy ML deps), docker compose config validation for both dev
and prod overlays, plus npm ci -> eslint -> tsc -> vite build for the
frontend.
ESLint config uses the v9 flat-config format that the project was
already on (eslint v9 dropped .eslintrc support); replaces the broken
'eslint . --ext' invocation and adds @typescript-eslint, react-hooks,
and react-refresh plugins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>