Without a separator, UA='foobar'+AL='' and UA='foo'+AL='bar' produced the same
HMAC input, allowing a token fingerprint to match a different header split.
Fix: use '\x00' as separator between user_agent and accept_lang fields.
Regression test added: test_fgp_no_concatenation_collision (FGP-05).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added "jti": str(uuid.uuid4()) to the access token payload in services/auth.py
- Promoted Wave 0 xfail stub test_create_access_token_includes_jti_claim to PASS
- Added test_create_access_token_jti_is_unique_per_call uniqueness guard
- uuid module was already imported at module top — no new imports needed
DocumentCard classification_failed badge requires doc.status from the
API list endpoint. _doc_to_dict was omitting the field; this fix adds
it and adds a regression test that would have caught the omission.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig)
- Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS
- load_provider_config() now returns ProviderConfig (no lazy import inside function body)
- classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06)
- Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06)
- Fallback to app_settings defaults when load_provider_config returns None (D-15)
- Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13)
- Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation)
- Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
- D-11: replace get_client_ip body with trusted-proxy CIDR check (127/8, 172.16/12,
192.168/16, ::1/128); untrusted peers always return their own IP, preventing XFF
spoofing by external callers
- D-12: create services/rate_limiting.py with _account_key() keyed by user.id
(falls back to peer IP when no authenticated user in request.state); exports
account_limiter = Limiter(key_func=_account_key)
- Wire: auth.py switches from get_remote_address to get_client_ip; main.py imports
account_limiter; all 9 documents.py endpoints and 7 cloud.py endpoints decorated
@account_limiter.limit("100/minute") with request.state.current_user = current_user
as the first handler statement (A1 ordering invariant)
- Promote all 8 xfail stubs to real assertions; add autouse fixture in conftest.py
to reset MemoryStorage between tests preventing cross-test 429 contamination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
shared by all AI providers; removes duplicated private functions from
anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
_validate_password_strength from admin.py and auth.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
str(uuid_obj) produces dashed 36-char format; SQLite stores UUID as 32-char
hex without dashes, so WHERE user_id = :uid never matched. Using .hex fixes
confirm_upload (api/documents.py) and delete_document (services/storage.py).
Removes stale xfail from test_delete_decrements_quota — now passes on SQLite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cloud_cache.py: module-level TTLCache(maxsize=1000, ttl=60) singleton with
threading.Lock for concurrent access safety (RESEARCH.md Pattern 8 / D-16)
- get_cloud_folders_cached(): async function; calls fetch_fn OUTSIDE the lock
to avoid blocking the event loop during cloud API calls
- invalidate_provider_cache(): removes all cache entries for a user+provider prefix
- storage/__init__.py: adds get_storage_backend_for_document() async factory
— returns MinIOBackend for minio docs; queries CloudConnection (scoped to user.id),
decrypts credentials, and lazy-imports cloud backends to avoid circular imports
— raises HTTPException(503) if connection missing or not ACTIVE (T-05-02-04)
Adds the unified file manager view (Windows Explorer-style), collapsible
folder tree sidebar item, full vitest test suite (55 tests, 4 files), and
commits all Phase 4 backend/frontend fixes that were staged but uncommitted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Creates backend/services/audit.py with write_audit_log() function
- Uses session.flush() not session.commit() per D-14 architectural requirement
- Catches and logs all exceptions (never re-raises) so audit failure is non-fatal
- Correct AuditLog ORM attribute metadata_ (not metadata) per models.py
Includes planning artifacts (03-CONTEXT, 03-DISCUSSION-LOG, 03-02-SUMMARY),
integration test script, MinIO/auth/docker fixes, and local dev account reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/celery_app.py: Celery("docuvault") with Redis broker, JSON
serialization, and tasks.document_tasks.* routed to documents queue;
reads REDIS_URL directly from os.environ (no config import — Pitfall 7)
- Add backend/tasks/__init__.py: empty package marker
- Add backend/tasks/document_tasks.py: sync extract_and_classify Celery task
that calls asyncio.run(_run()) to retrieve bytes from MinIO, extract text
via extractor, and classify via classifier; classification failure is non-fatal
- Update backend/services/classifier.py: classify_document and
suggest_topics_for_document now accept session: AsyncSession as first arg;
all storage.* calls updated to async session-injection pattern
- Add extract_text_from_bytes helper to services/extractor.py for bytes-based
extraction (used by Celery worker, which retrieves bytes from MinIO)