Why:
- resolve_auth_context() runs on every authenticated request and logged
nothing; four distinct rejection reasons (malformed/unknown/inactive/
expired key, inactive tenant) were all invisible.
- The domain-allowlist rejection in upload_source_file() happens before any
ingestion_jobs row exists, so it wasn't covered by the job-level
ingestion.job.failed event either -- a rejected upload left zero trace.
- Four of upload_source_file()'s five failure branches (parse_failed,
chunk_limit_exceeded, embedding_failed, index_failed) called
_mark_job_failed(), which wrote to Postgres but never logged; only
storage_failed and timeout had an ad-hoc logger.warning duplicated at their
own call sites.
Changes:
- auth/service.py: auth.succeeded / auth.failed (with a reason field per
rejection type), matching ADR-0011's own event catalog.
- domains/service.py: domain.rejected on the allowlist check;
domain.created / domain.updated / domain.status_changed on the three
mutations.
- files/upload.py: centralized failure logging inside _mark_job_failed
(every failure branch already calls it, so logging there once closes all
five branches instead of duplicating a log call at each site) as
ingestion.job.failed; added ingestion.job.started; renamed the ad-hoc
files.upload.succeeded to ingestion.job.completed for catalog consistency.
Impact:
- None to request/response behavior -- log events only.
Why:
- Domain values are denormalized into every Qdrant point payload. Without
validation, an unregistered or typo'd domain (e.g. "fier" for "fire")
silently creates a new partition that retrieval never queries — the file
ends up invisible rather than rejected. Tenants also need independently
sized domain sets (one may run 14 insurance lines, another 6), which rules
out an enum.
Changes:
- tenant_domains table (migration 41335d162de8) + repository, unique on
(tenant_id, domain).
- src/application/domains/: ensure_domain_allowed() is the strict-allowlist
check now run inside upload_source_file()'s first transaction, before any
MinIO object, job row, or Qdrant point is written.
- /v1/domains (list/create/patch/disable/enable) gated on its own
domains:read/domains:write scopes, deliberately separate from files:write
so an upload key cannot create partitions. domain itself is immutable
(denormalized into every point payload); only display_name is editable.
Disable blocks new uploads without touching already-indexed points.
Impact:
- BREAKING: POST /v1/files now rejects any domain without an active
tenant_domains row (400, unknown_domain). A domain must be created via
POST /v1/domains before the first upload to it.