# JobTracker audit remediation backlog

Prepared: 2026-08-02

No remediation has been implemented. Ordering follows exploitable security, cross-user exposure, data integrity, broken core workflows, production reliability, regression protection, accessibility/usability, maintainability, then optional hardening.

No cross-user exposure was confirmed, so the backlog does not invent one. Every item references validated findings and keeps unrelated work deferred.

## Phase 0 — validated implementation design

This design revalidates the Phase 0 findings against their complete call paths, current settings, Docker/deployment behavior and existing tests. It is a design only: no application code, dependency, configuration, schema or migration has been changed.

### Revalidation record

- Worktree before design: branch `release-readiness`; existing `D .agent.md`, `?? AGENTS.md` and `?? docs/audits/` were preserved.
- End-of-design status also showed unrelated `?? docs/todo/`; it was not modified. No existing work was discarded, overwritten or committed.
- All files under `docs/audits/`, including every evidence file, were read before revalidation.
- Targeted .NET baseline: `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~MicrosoftTokenValidatorTests|FullyQualifiedName~AuthAndSystemControllerTests|FullyQualifiedName~SessionsControllerTests|FullyQualifiedName~AttachmentsControllerTests|FullyQualifiedName~AttachmentFlagsRecomputeTests|FullyQualifiedName~ProfileCvControllerTests|FullyQualifiedName~ProductionConfigTests" --logger "console;verbosity=minimal"` — **73 passed, 0 failed**. Some assertions intentionally describe the current unsafe behavior; passing is not remediation evidence.
- Python parser baseline: `python -m pytest tools/summarizer/tests -q` — **17 passed, 0 failed**, with five SWIG deprecation warnings.
- The production Compose merge was inspected without expanding secret values. Because `deploy/deploy.sh` invokes plain `docker compose`, `docker-compose.override.yml` is auto-loaded: the merged configuration publishes backend `5202:8080` and frontend `3000:80`, and sets backend proxy trust to `false`. This materially strengthens the JT-002 deployment concern.
- No malicious file was parsed. Advisory presence and parser reachability are confirmed; exploitation is not.

### Ordering decision and dependency graph

The recommended sequence requires one correction. JT-002 must precede legacy Microsoft relinking and pending-email/recovery links: those flows cannot safely prove ownership while security links can still inherit an attacker-controlled Host. Session-revocation primitives should also land before email-change and recovery transitions use them.

| Execution order | Work package | Findings | Reason |
|---|---|---|---|
| 1 | **P0-2A** Canonical application origin and Host guard | JT-002 | Establishes the trusted URL boundary needed by identity recovery. |
| 2 | **P0-2B** Production ingress, proxy and Compose alignment | JT-002 | Removes auto-loaded development exposure and defines the Traefik/nginx trust chain. |
| 3 | **P0-1A** Microsoft issuer/tenant validation | JT-001 | Stops accepting identities without a tenant-qualified key; no database change. |
| 4 | **P0-1B** Canonical Microsoft links and legacy relinking | JT-001 | Additive migration plus a non-merging transition after safe links exist. |
| 5 | **P0-4A** Session invalidation primitives and recovery use | JT-008 | Shared concrete revocation behavior first, without a speculative abstraction. |
| 6 | **P0-4B** Registration and pending-email state machine | JT-007/JT-008 | Uses canonical links and the revocation primitive. |
| 7 | **P0-3A** Parser dependency compatibility update | JT-006 | Isolates dependency compatibility from behavioral hardening. |
| 8 | **P0-3B** Bounded parser subprocess and safe fallback | JT-006/JT-011 | Adds input/CPU/memory/time limits and removes the unsafe backend fallback. |
| 9 | **P0-3C** Container hardening and abandoned-work cleanup | JT-006 | Deployable separately after measured resource sizing. |
| 10 | **P0-5** Recoverable attachment mutations | JT-010 | Establishes file/row invariants reused by account deletion. |
| 11 | **P0-6A** Owner-scoped export inventory and readable export | JT-009 | Makes ownership explicit before deletion uses it. |
| 12 | **P0-6B** Idempotent deletion lifecycle and backup tombstones | JT-009 | Last because it depends on identity, attachment and ownership invariants. |

P0-3 can proceed in parallel with P0-1/P0-4 after P0-2; it has no identity dependency. P0-5 can also proceed after its storage journal format is coordinated with P0-6. P0-6B must not precede P0-5 or P0-6A.

### JT-001 — Microsoft tenant/issuer validation and safe identity linking

#### Revalidated finding

- **Final severity/confidence/classification:** **High / High / likely defect**. The acceptance and auto-linking paths are confirmed. A production account takeover was not attempted, and exploitability still requires a token for the configured client plus a colliding trusted email or legacy identifier.
- **Confirmed execution path:** `job-tracker-ui/src/components/MicrosoftAuthCard.tsx:27-30,74-83` uses MSAL authority `common`, obtains an ID token and posts it to `/auth/microsoft/exchange` or `/auth/microsoft/link`. `JobTrackerApi/Services/MicrosoftTokenValidator.cs:26,57,72-90,97-99` loads `common` discovery, validates signature/audience/lifetime with `ValidateIssuer = false`, performs only a login.microsoftonline.com issuer-shape check, selects `oid` then `sub`, and treats `email` or `preferred_username` as a verified email. `JobTrackerApi/Controllers/AuthController.cs:285-344` finds an account by `MicrosoftSubject` **or `MicrosoftEmail`,** then auto-links an existing local user returned by `FindByEmailAsync`; `:515-583` applies the same subject-or-email collision rule during explicit linking. `JobTrackerApi/Models/ApplicationUser.cs:19-21` stores only `MicrosoftSubject`, `MicrosoftEmail` and link time. `JobTrackerApi/Program.cs:371-381` exposes a second raw Microsoft bearer scheme using `common` and disabled issuer validation even though the UI exchanges ID tokens for local sessions.
- **Existing mitigations:** Microsoft token signature, audience and lifetime are validated; issuer hostname/path shape is checked; local registration can be disabled; duplicate app emails are constrained by Identity normalization; explicit linking requires a local authenticated session. These do not bind the external identity to a tenant or make mutable email a safe account key.
- **Existing tests:** validator tests cover accepted issuer shape and an unrelated issuer; controller tests cover new-user exchange and disabled registration. They do not cover `tid`, issuer/tenant mismatch, same `oid` across tenants, email collisions, or a safe legacy transition.

#### Stable identity and configuration contract

- The relationship is owned by the verified pair **(`tid`, `oid`)**, each parsed and stored as a normalized GUID string. `oid` alone is tenant-scoped; `sub` and email/`preferred_username` must never own or merge the relationship.
- The exact expected issuer is `https://login.microsoftonline.com/{tid}/v2.0`; the signed `tid` must equal the issuer path tenant and satisfy the configured account mode.
- Add one sign-in setting, `Auth:MicrosoftTenant`, distinct from `Microsoft:TenantId` used for Graph mailbox OAuth:
  - GUID: single-tenant, exact `tid` only.
  - `organizations`: Entra organizational tenants; reject personal Microsoft accounts.
  - `consumers`: personal Microsoft accounts only.
  - `common`: explicit organizational plus personal multitenant support.
- Production with Microsoft sign-in enabled must specify this setting. Development/Test may explicitly use `common`; any backward-compatible default is Development/Test-only and emits a warning.
- The validator requires GUID-shaped `tid` and `oid`, exact issuer/tenant agreement, configured audience, signature and lifetime. It returns tenant ID, object ID and display/email claims as metadata. It does **not** assert that an email claim proves mailbox ownership.
- Remove the unused raw Microsoft bearer branch from the smart authentication policy instead of maintaining two trust decisions. If an undiscovered API consumer needs it, it must be documented and validated by the same tenant policy before removal is reconsidered.

#### Existing-link transition and migration

- Add nullable `MicrosoftTenantId` and `MicrosoftObjectId` columns to `AspNetUsers`, bounded to 36 characters, with a filtered/nullable unique composite index. Keep `MicrosoftSubject` and `MicrosoftEmail` temporarily as legacy evidence and rollback metadata; stop writing `MicrosoftSubject` after cutover. Do not index or reinterpret it because existing values may be `oid` or `sub` and the current provider cannot prove their tenant retrospectively.
- Before deployment, produce counts only: total legacy links, duplicate legacy subjects/emails, and whether each affected account has an alternate password or Google credential. Do not print claim or email values.
- Do **not** backfill tenant IDs from email, `common`, Graph mailbox settings or the next token seen. Unknown tenant means unknown ownership.
- A currently authenticated local user may explicitly relink after fresh Microsoft authentication. The canonical pair must be unused; email similarity is informational only.
- A social-only legacy user needs a one-time recovery ceremony: a valid tenant-qualified Microsoft token **and** a purpose-bound proof sent to the already stored, confirmed application email. The recovery token binds application user ID plus proposed `tid`/`oid` and expires. Multiple candidates, an unconfirmed/unavailable email, or any collision moves to operator-assisted identity verification; the system never silently merges accounts.
- New Microsoft registration binds (`tid`, `oid`) immediately. The provider email may prefill the app email but remains `EmailConfirmed = false`; when app email verification is required, no local session is issued until it is verified. If verification is disabled, the Microsoft identity may establish the session, but recovery/notification behavior still treats the mailbox as unverified.
- Single-tenant deployments reject all other `tid` values. Multitenant deployments retain one local account per canonical pair; two tenants presenting the same email remain separate unless an already authenticated user explicitly links and proves both sides.

#### Implementation contract

- **Proposed design:** extend the existing validator and controller directly; add no one-implementation identity-provider framework. Centralize one tenant policy/value parser used by exchange and explicit link. Change lookup/conflict checks to the canonical composite key only. Require recent local reauthentication for link/unlink, and refuse unlink if it would remove the last usable sign-in credential. Treat the legacy relink flow as a temporary, separately gated endpoint that can be removed after the migration window.
- **Affected files/components:** `JobTrackerApi/Services/MicrosoftTokenValidator.cs`, `JobTrackerApi/Controllers/AuthController.cs`, `JobTrackerApi/Program.cs`, `JobTrackerApi/Models/ApplicationUser.cs`, `JobTrackerApi/Data/ApplicationDbContext.cs`, a new EF migration and snapshot update, auth DTOs, `job-tracker-ui/src/components/MicrosoftAuthCard.tsx`, login/link UI, `.env.example`, `appsettings*.json`, deployment environment validation, and Microsoft/auth tests.
- **Database/configuration migration:** additive nullable columns and unique index first; no destructive backfill. Add the required production tenant-mode setting. Keep legacy columns through at least one verified release and the relinking window. Do not add these columns to the startup schema reconciler as a second schema owner.
- **Backward-compatibility risks:** legacy Microsoft-only users cannot be transparently mapped; stricter tenant modes may reject accounts previously accepted through `common`; email-collision users may see a new-account/recovery choice; removing raw bearer support can affect undocumented clients. Inventory and a temporary relink window reduce lockout without accepting unsafe fallback behavior.
- **Rollback:** disable Microsoft exchange/link endpoints with an existing-style kill switch; leave local/password/Google login available. Roll back application binaries while additive columns and legacy fields remain. Never roll back to email auto-linking. If a canonical link has been created, do not delete or reassign it during rollback.
- **Deployment sequence:** (1) finish P0-2A; (2) inventory legacy rows and alternate credentials; (3) deploy tenant validation and raw-bearer removal; (4) apply the additive migration; (5) deploy canonical lookups plus relink UI disabled; (6) enable relinking for affected users, monitor collision/lockout counters without PII; (7) close the transition endpoint after the documented period; (8) remove legacy columns only in a later approved migration.
- **Documentation changes:** supported account-mode matrix, separate sign-in versus Graph tenant settings, operator relink/recovery procedure, collision behavior, raw-bearer removal, and user-facing explanation that Microsoft email does not automatically merge an account.
- **Dependencies:** P0-2A for safe recovery URLs; P0-4A/B for recent reauthentication, email proof and revocation. The validator-only P0-1A can land before P0-4, while migration/relink P0-1B cannot be considered complete before it.

#### Required tests and acceptance criteria

- Unit-test each mode with valid and invalid `tid`; missing/non-GUID `tid`/`oid`; exact issuer mismatch; wrong audience/signature/lifetime; personal versus organizational tenant; and the same `oid` in two tenants.
- Integration-test new account, existing canonical link, canonical-pair collision, same email across tenants, explicit link/unlink with recent/expired reauthentication, 2FA account, last-credential guard and disabled registration.
- Migration-test fresh SQLite/MariaDB and representative legacy rows with null/duplicate/ambiguous legacy values. Assert no legacy row is silently assigned or merged.
- End-to-end test a new Microsoft account and a synthetic legacy relink with mocked tokens and email. No real Microsoft account or email is used.
- **Acceptance:** every accepted token has a verified configured tenant and exact issuer; account lookup is solely (`tid`, `oid`); email cannot auto-link; ambiguous legacy rows remain unlinked and recoverable; no unrelated identities merge; single/multitenant behavior matches the documented matrix; and Microsoft-only users have a tested non-silent recovery path.

### JT-002 — canonical external origin and Host-header handling

#### Revalidated finding

- **Final severity/confidence/classification:** **High / High / likely defect**. Code-level Host poisoning is confirmed. Production exploitability is not claimed as confirmed because the external Traefik/firewall configuration is absent, but repo-defined deployment exposes direct backend/frontend ports and makes the prerequisites realistic.
- **Confirmed execution path:** `JobTrackerApi/Controllers/AuthController.cs:695-701,806-812` and `JobTrackerApi/Controllers/UsersController.cs:149-155` build verification/reset/admin-reset links from `App:PublicBaseUrl` and fall back to `Request.Scheme`/`Request.Host`. `GmailController.cs:1013-1023` and `MicrosoftGraphController.cs:112-122` do the same for OAuth callbacks; `BillingController.cs:223-230` requires the base URL, while `FollowUpReminderHostedService.cs:48` accepts older aliases. `JobTrackerApi/appsettings.json` has `AllowedHosts: "*"`. nginx accepts `server_name _`, forwards `$host`, and overwrites forwarded proto with its internal `$scheme`. `JobTrackerApi/Program.cs:454-464` enables one-hop forwarded processing only when configured and clears known proxies/networks. `docker-compose.yml:18,52` enables proxy trust and passes the possibly blank origin, but the auto-loaded `docker-compose.override.yml:5` disables proxy trust and publishes `5202`/`3000`; `deploy/deploy.sh` invokes plain Compose. Repository evidence contains no Traefik router/host allowlist.
- **Existing mitigations:** `APP_PUBLIC_BASE_URL` can provide a safe origin; token links expire and still require victim action; production can be protected by an external exact-host router/firewall; forwarded-header limit is one. These mitigations are optional, undocumented as a hard contract, or contradicted by the merged Compose deployment.
- **Code exposure versus exploitability:** any request that reaches a link-generating endpoint with blank `PublicBaseUrl` can influence the generated origin. Exploitation requires reachability using an untrusted Host, email delivery, and a recipient following the link. A correctly configured unobserved Traefik/firewall could prevent that, so the path is a confirmed code defect and a high-confidence deployment risk rather than a reproduced production compromise.

#### Canonical origin and proxy design

- Reuse `App:PublicBaseUrl` as the **only** external-origin setting. At startup parse it once into a concrete immutable value; do not create an interface/factory for one value.
- Production requires an absolute HTTPS URL with no userinfo, query, fragment or non-root path. Normalize scheme, ASCII host and explicit non-default port once. Development/Test may use an explicit `http://localhost:3000`; no environment may fall back to request headers for security links.
- All verification/reset/admin reset URLs, OAuth callbacks, billing redirects, reminder links and absolute frontend links use the parsed value. Cookie `Secure` behavior in Production follows the canonical HTTPS origin, not an untrusted header.
- Derive the application Host allowlist from that canonical host rather than introducing a second public-host setting. In Production reject unknown API Host values with 400/421 before authentication/routing. Permit only narrowly documented internal health names (`backend`, `localhost`, `127.0.0.1`) for internal health probes; they must never be used to generate links.
- The production Compose command must name only `docker-compose.yml`; move/rename the auto-loaded override to an explicitly selected development file. Production must not publish backend or frontend host ports. Traefik reaches only the frontend on `shared_services`; nginx reaches backend on the private application network.
- Traefik's operator contract is an exact canonical `Host()` rule, TLS termination, no direct published application ports, and replacement of client forwarding headers. nginx must pass the sanitized external Host/proto rather than replace proto with its internal HTTP scheme. Backend forwarded-header processing must trust explicit proxy IP/network configuration; never clear both trusted proxy collections. Document the actual two-hop Traefik → nginx → backend chain and its forwarding limit.

#### Implementation contract

- **Proposed design:** P0-2A adds startup validation, one shared origin value, caller replacement and application Host filtering. P0-2B changes Compose selection/exposure, nginx forwarding and explicit proxy trust. This fixes the root origin source once and removes request-specific guards.
- **Affected files/components:** `Program.cs`; auth, admin-user, Gmail, Microsoft Graph and billing controllers; follow-up hosted service; URL/config helpers; `appsettings*.json`; `.env.example`; `docker-compose.yml`, development override naming, `deploy/deploy.sh`, `deploy/README.md`; nginx configuration; production/config/controller tests.
- **Database/configuration migration:** no database migration. `APP_PUBLIC_BASE_URL` becomes required in Production. Add explicit trusted proxy/network values if forwarded headers are enabled. Any Traefik labels/config live in the operator deployment and must be supplied before production exploitability can be closed.
- **Backward-compatibility risks:** environments relying on blank base URL will fail fast; noncanonical aliases or direct port access will be rejected; incorrect proxy IP/network values can cause wrong client IP/scheme; OAuth registered redirect URIs must exactly match the canonical callbacks. Local/Test defaults must remain explicit and isolated.
- **Rollback:** restore the last known valid canonical URL/proxy configuration and prior image if necessary, but do not restore request-host fallback or wildcard production Host acceptance. Keep the backend unexposed during rollback. A config validation failure should stop deployment before traffic shifts.
- **Deployment sequence:** (1) inventory the production URL, OAuth redirect URIs, proxy network/IP and firewall; (2) deploy P0-2A with canonical setting supplied and test canonical/hostile hosts; (3) update provider redirect registrations if necessary; (4) deploy P0-2B using explicit production Compose files; (5) verify no bound `3000/5202` ports, exact Traefik routing and sanitized headers; (6) run email/OAuth smoke tests with mocks/non-sending sinks; (7) monitor rejected Host and forwarded-header warnings without logging tokens.
- **Documentation changes:** make the origin and exact ingress contract authoritative; remove conflicting one-hop claims; add local/dev Compose commands, production Compose command, required variables, host/proxy smoke checks and OAuth callback examples.
- **Dependencies:** P0-2A is a prerequisite for JT-001 relinking and JT-007 email change/recovery. P0-2B is operationally coupled but has no database dependency.

#### Required tests and acceptance criteria

- Unit/config tests for missing/HTTP/malformed/userinfo/query/fragment/path production URLs, IDN/Unicode host normalization, ports, and local/Test HTTP behavior.
- Controller tests send hostile `Host`, `X-Forwarded-Host` and `X-Forwarded-Proto` values and assert every emitted absolute URL remains canonical.
- Host-filter tests assert canonical API requests pass, unknown hosts fail, internal health probes work without becoming link origins, and direct backend Host spoofing is rejected.
- Deployment tests inspect merged production and development Compose output without revealing environment values; Production has no bound application ports and does not auto-load the development override.
- Proxy integration tests cover the two-hop TLS request, exact forwarded proto/host, explicit known proxy, unknown proxy header rejection and secure cookies.
- **Acceptance:** Production fails before serving when canonical origin/proxy trust is invalid; request/forwarded hosts never influence outbound URLs; unknown production hosts are rejected at ingress and application; direct application ports are closed; OAuth/email URLs match registrations; local development and test startup remain documented and green.

### JT-006 — document-parser upgrades and resource isolation

#### Revalidated finding and package inventory

- **Final severity/confidence/classification:** **High / High / likely defect**. Vulnerable versions and reachable untrusted parser paths are confirmed. Successful exploitation, denial of service or code execution is **not** confirmed and was not attempted.
- Manifest pins: FastAPI 0.115.12, Uvicorn 0.34.0, Transformers 4.48.3, cachetools 5.5.2, Pydantic 2.10.6, Torch 2.6.0, Pillow 11.1.0, pytesseract 0.3.13, pypdf 5.4.0, PyMuPDF 1.25.5, python-docx 1.1.2 and python-multipart 0.0.20. Audited transitive Starlette is 0.46.2. The current shared workstation has Pydantic 2.13.4, demonstrating environment drift; the deployment manifest remains authoritative.
- Reachable advisory snapshot from the audit evidence:

| Package | Installed | Advisory/CVE identifiers | Highest audited fixed floor |
|---|---:|---|---:|
| Pillow | 11.1.0 | PYSEC-2026-165, PYSEC-2026-2250, PYSEC-2026-2253, PYSEC-2026-2255, PYSEC-2026-2257, PYSEC-2026-2256, PYSEC-2026-2254, PYSEC-2026-2252, PYSEC-2026-2249, PYSEC-2026-2874, PYSEC-2026-3453, PYSEC-2026-3451, PYSEC-2026-3454, PYSEC-2026-3495, PYSEC-2026-3496, PYSEC-2026-3494, PYSEC-2026-3493 | 12.3.0 |
| pypdf | 5.4.0 | PYSEC-2026-1833, PYSEC-2026-1829, PYSEC-2026-1832, PYSEC-2026-1830, PYSEC-2026-1831, PYSEC-2026-1827, PYSEC-2026-1828, PYSEC-2026-3023, PYSEC-2026-3022, PYSEC-2026-3017, PYSEC-2026-3019, PYSEC-2026-3020, PYSEC-2026-3018, PYSEC-2026-3021, PYSEC-2026-3011, PYSEC-2026-3007, PYSEC-2026-3004, PYSEC-2026-3005, PYSEC-2026-3006, PYSEC-2026-3014, PYSEC-2026-3024, PYSEC-2026-3026, PYSEC-2026-3016, PYSEC-2026-3010, PYSEC-2026-3015, PYSEC-2026-3025, PYSEC-2026-3013, PYSEC-2026-3009, PYSEC-2026-3012, PYSEC-2026-3027, GHSA-jm82-fx9c-mx94, CVE-2026-59938, CVE-2026-59937, CVE-2026-59935, CVE-2026-59936 | 6.14.2 |
| python-multipart | 0.0.20 | PYSEC-2026-1852, PYSEC-2026-3038, PYSEC-2026-3037, PYSEC-2026-3036, PYSEC-2026-3040, PYSEC-2026-3039 | 0.0.31 |
| Starlette | 0.46.2 | PYSEC-2026-161, PYSEC-2026-248, PYSEC-2026-249, PYSEC-2026-1942, PYSEC-2026-1941, PYSEC-2026-2281, PYSEC-2026-2280 | 1.3.1 |

Fixed floors are advisory-clearing candidates, not a compatibility approval. FastAPI 0.115.12 constrains Starlette to `<0.47`; therefore Starlette 1.3.1 cannot be installed with the current FastAPI pin. The exact compatible fixed FastAPI/Starlette pair remains **unverified** until approved package-index/advisory resolution is performed. Do not guess it. Model-stack advisories are tracked under JT-017 and are not used to inflate JT-006 unless a reachable model-loading path is shown.

#### Confirmed parser path and mitigations

- `JobTrackerApi/Controllers/ProfileCvController.cs:138-194` accepts an authenticated CV, enforces an extension and 5 MiB application limit, writes an artifact/run row and synchronously calls extraction. `ProfileCvController.Pipeline.cs:176-232` stores/dispatches it, and `JobTrackerApi/Services/SummarizerService.cs:342-390` sends multipart to `/extract-text` with a 30-second HTTP timeout.
- Starlette/python-multipart accepts the upload; `tools/summarizer/app.py:868-899` then performs `await file.read()` before its 8 MiB check. `:832-858` uses pypdf text extraction and, when text is sparse, renders every page at 2x through PyMuPDF/Pillow/Tesseract. Images use Pillow/Tesseract; DOCX uses python-docx. There are no page, dimension, decoded-pixel, decompression or per-stage CPU/memory limits.
- If the sidecar fails, `JobTrackerApi/Controllers/ProfileCvController.Parsing.cs:1276-1310` reads the whole bounded file; DOCX opens ZIP `word/document.xml` without entry/decompression/ratio limits. Isolating only Python would leave this parser path reachable.
- The in-memory CV queue is unbounded. Database runs let startup discover interrupted work, but parser temporary files/processes are not durably tracked. Raw exception messages can be stored/emailed.
- **Existing mitigations:** authentication/ownership, backend 5 MiB request/file limit, extension allowlist, 30-second client timeout, service token, private `ai_internal` network, no sidecar host port, artifact/run status and retention pruning. These reduce reachability but do not bound decoded work or terminate parser descendants. The Python container currently runs as root, uses mutable `python:3.11`, and lacks read-only/non-root/PID/CPU/memory controls.

#### Bounded-processing design

- P0-3A resolves and pins a compatible fixed dependency set. Initial candidate floors are pypdf 6.14.2, Pillow 12.3.0 and python-multipart 0.0.31; choose FastAPI/Starlette together only after resolver and benign-corpus verification. Produce a lock/hash artifact and record any accepted exception. No package is changed in this design phase.
- Validate content with standard-library signature/container checks before parser dispatch: `%PDF-`; PNG/JPEG/WebP signatures; DOCX ZIP containing `[Content_Types].xml` and `word/document.xml`; bounded text encoding/no-NUL rules. Extension and detected type must agree.
- Use one 5 MiB file-content cap at backend and sidecar, a 6 MiB HTTP/multipart cap for framing overhead, and bounded chunked reads into a private spool file; never `await file.read()` an untrusted upload. Align nginx `client_max_body_size` without making it the only enforcement.
- Initial configurable ceilings, selected to be testable rather than unlimited: 40 PDF pages; 200,000 extracted characters; 256 DOCX ZIP entries; 32 MiB total declared uncompressed ZIP content; 8 MiB largest entry/`document.xml`; 100:1 compression ratio; 12,000 pixels per image dimension; 40 megapixels per image; single-frame raster images; 160 MiB maximum decoded image bytes; 12 megapixels rendered per PDF page and 120 megapixels total OCR work.
- Run each parse in a child process with a minimal environment and no AI-provider keys. On Linux use `resource.setrlimit` for CPU (20 seconds), address space (initial 512 MiB for the parser child), output/file size and open descriptors. Apply a 25-second parent deadline, start a new process group and kill the group on timeout so Tesseract descendants do not survive. Limit parent concurrency to one parse initially; measure before increasing it.
- The 512 MiB child limit does not become a guessed whole-sidecar limit: the parent also hosts ML models. Measure its healthy resident set and assign a container memory ceiling with explicit headroom in P0-3C. Use a private `0700` temp directory, `0600` UUID spool files, `finally` deletion and startup cleanup of files older than one hour; mount temp storage as size-bounded tmpfs.
- Remove nontrivial backend PDF/DOCX/image fallback. A bounded plain-text fallback may remain. If the isolated parser is unavailable, fail safely with a stable 422/503 code; do not re-enter an unisolated parser.
- Bound the in-memory channel or return backpressure. Mark interrupted/expired runs failed or retryable with stable error codes. Never store/email raw parser exception text or paths. Reconciliation removes stale temp work and applies the existing artifact retention policy.
- P0-3C runs the sidecar as non-root with read-only root filesystem, `cap_drop: ALL`, `no-new-privileges`, tmpfs, PID/CPU/memory limits and the existing private network/service token. Child-process isolation in one container is the minimum design; a separate parser service is deferred unless measurement or platform limitations show that this boundary is insufficient.

#### Implementation contract

- **Proposed design:** P0-3A proves one compatible fixed dependency set; P0-3B moves untrusted decode into the bounded child process and removes binary/DOCX fallback; P0-3C applies measured container controls. Reuse Python/OS standard process and resource controls and existing request-size facilities before considering a new sandbox service or package.
- **Affected files/components:** `tools/summarizer/requirements.txt` and a generated lock/hash file; `tools/summarizer/app.py` plus a small parser-child module; summarizer Dockerfile/Compose settings; `ProfileCvController` upload/pipeline/parsing partials; `SummarizerService`; `CvProcessingQueue`; nginx upload settings; parser/backend tests and benign fixtures.
- **Database/configuration migration:** no required schema migration. Add explicit parser-limit/environment settings with the conservative defaults above and container runtime budgets. If a durable retry field proves necessary, it requires a separate additive migration and review rather than being smuggled into the hardening patch.
- **Backward-compatibility risks:** large/page-heavy/image-heavy CVs that previously attempted processing will be rejected; extraction output can change after parser upgrades; OCR may time out; non-Linux developer environments cannot enforce the same rlimits and must fail/skip explicitly in tests; removing fallback reduces availability when the sidecar is down.
- **Rollback:** retain the previous image digest for diagnosis, but disable CV import/extraction rather than re-expose a known vulnerable/unbounded parser. Database artifacts remain intact and can be retried after a corrected image. Roll back individual conservative limits by configuration only after measured benign evidence.
- **Deployment sequence:** (1) build P0-3A image and run dependency audit/resolution plus benign regression corpus; (2) deploy to a non-production local environment with parsing disabled by default; (3) add P0-3B boundary/failure tests and enable for synthetic files; (4) size healthy parent/child memory; (5) deploy P0-3C runtime controls; (6) canary enable CV import, monitor timeouts/rejections/memory without logging content; (7) widen only evidenced-too-small limits through reviewed config.
- **Documentation changes:** exact formats/limits/error codes; parser/subprocess/container trust boundary; temporary-file cleanup; dependency/advisory exception record; safe update procedure and regression corpus; operational disable/retry procedure.
- **Dependencies:** P0-3A before P0-3B/C; P0-2 is not a parser prerequisite. Coordinate account-deletion cleanup with P0-6, but do not delay vulnerable dependency removal for it.

#### Required tests and acceptance criteria

- Dependency resolution/audit test shows no unaccepted reachable High/Critical parser advisory; assert the final FastAPI/Starlette pair is compatible. Clean-image install uses locked versions/hashes.
- Generated benign boundary fixtures test exact/over file, page, ZIP entry/uncompressed/ratio, dimension/pixel, character and multipart limits; extension/magic mismatches; truncated/corrupt files; Unicode/Norwegian text; and a normal CV corpus for extraction quality.
- A harmless sleeping child tests timeout/process-group termination; generated oversized metadata tests resource rejection without malicious payloads. Assert temp files and children are gone after success, error, cancellation and simulated restart.
- Backend integration tests assert sidecar outage never invokes PDF/DOCX/image fallback, errors are stable/sanitized, queue backpressure works and owner isolation remains.
- Container tests assert non-root, read-only, dropped capabilities, private network, token requirement, bounded tmpfs/PIDs/resources and no published port.
- **Acceptance:** untrusted input is bounded before buffering and before expensive decode; every parser descendant is terminated on deadline; failed/abandoned work is cleaned; no raw parser details leak; normal corpus remains acceptable; and any remaining advisory has an explicit reviewed exception and compensating control. No claim of exploit reproduction is required.

### JT-007 and JT-008 — email ownership and session invalidation

#### Revalidated findings

- **JT-007 final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. With email verification enabled, registration issues a normal local session before confirmation; profile update changes email/username without ownership proof and leaves confirmation state unchanged. Runtime evidence confirms protected access and the retained confirmation flag.
- **JT-008 final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. Logout clears cookies only; a copied cookie remains valid. Successful password reset/change does not revoke existing `UserSessions` or trusted devices.
- **Confirmed execution paths:** `JobTrackerApi/Controllers/AuthController.cs:132-180` creates an unconfirmed Identity user, sends verification, then calls `CompleteSignInAsync`; login later rejects unconfirmed users, so initial and later behavior conflict. Verification/resend are at `:748-799`. `UpdateProfile` at `:405-439` assigns email and username directly. Logout `:347-355`, password change `:655-674`, and reset `:678-743` contain no session-revocation call. `JobTrackerApi/Services/AppSessionIssuer.cs` creates a database `sid`; `Program.cs` token validation, `LocalSessionValidator.cs` and `SessionsController.cs` prove a revocation mechanism already exists.
- **Existing mitigations:** later login checks confirmation; Identity email/password tokens expire and are data-protected; session rows can be listed/revoked manually; signed local JWTs bind user and `sid`; 2FA/recovery codes/trusted devices exist. The current validator should still query by both `sid` and principal user ID defensively.
- **Existing tests:** tests currently expect registration success/session in relevant configurations and direct profile email update; session tests cover explicit revocation but not logout/reset/change revocation. These tests must be changed deliberately, not merely supplemented.

#### Complete state-transition contract

| Event | Preconditions | State/result | Session/trusted-device effect |
|---|---|---|---|
| Register, verification required | unique valid email/password | `PendingVerification`; send link; return 202 `verification_required`; no protected session | create no `UserSession`, session cookie or CSRF cookie |
| Verify registration email | valid latest Identity token for same user/email | `Active`; require a normal login, no implicit sign-in | none to revoke |
| Resend verification | unconfirmed account; generic response; rate limit | invalidate naturally via security stamp/latest-token policy where supported; send only to stored address | none |
| Register, verification disabled | normal policy | `Active` and existing sign-in behavior | create normal session |
| Request email change | authenticated + recent reauthentication; unique new email | retain old email/confirmation; store `PendingEmail` and request time; notify new address and old address | current sessions continue until proof |
| Confirm email change | valid token and exact current `PendingEmail`; user still active | atomically use Identity `ChangeEmailAsync`; update username only if it equalled old normalized email; clear pending state | revoke **all** sessions and trusted devices; require login with new address |
| Cancel/replace email change | authenticated; latest request wins | clear/replace pending; old email remains active | no revocation |
| Logout | valid, expired or malformed current cookies | best-effort revoke matching (`sid`, user); always clear session/CSRF cookies; return idempotent 204 | current session only; trusted device retained |
| Change password | authenticated + current password + 2FA where policy requires | update password/security stamp | revoke all **other** sessions; retain reissued current session; trusted devices other than current are removed |
| Request reset | generic response; only confirmed local-password accounts receive mail | send purpose-bound token to stored confirmed email | no revocation until success |
| Complete reset | valid token/new password | update password/security stamp; preserve 2FA requirement | revoke all sessions and all trusted devices; require login and normal 2FA/recovery code |
| Admin password reset | authorized admin, not unsafe self/last-admin case | same credential/security-stamp change | revoke all target sessions/trusted devices |
| Provider link/unlink or 2FA disable/recovery-code regeneration | recent reauthentication; never remove last credential | apply security-sensitive change | revoke other sessions; remove trusted devices when 2FA assurance changed |

Email reset is not a 2FA bypass. Passwordless accounts do not receive a password-reset flow unless a separately designed credential-creation path is approved. A provider token may satisfy recent reauthentication only through an already-linked immutable provider identity, never through a matching email claim.

#### Implementation contract

- **Proposed design:** reuse the existing session table/validator. Add concrete revocation methods for current, other and all sessions plus trusted-device removal; no interface is needed. Make logout `AllowAnonymous`/idempotent so stale cookies can always be cleared. Include user ID in session lookup. Update the security stamp on recovery-sensitive changes and bind pending 2FA state to the current stamp/version so old pending challenges fail.
- Remove email from generic profile update. Add request/confirm/cancel email endpoints using Identity's email-change token API. Store nullable `PendingEmail` and `PendingEmailRequestedAtUtc` so the current request is visible and latest-wins behavior is enforceable. Old confirmed email remains authoritative until atomic confirmation.
- Registration with required verification returns a typed 202 and never calls session issuance. The UI displays resend/verify guidance and does not call `/auth/me` or navigate as authenticated.
- **Affected files/components:** `AuthController`, profile/auth DTOs, `AppSessionIssuer`, `LocalSessionValidator`, `SessionsController`, trusted-device/2FA services, `ApplicationUser`, `ApplicationDbContext`, additive EF migration/snapshot, login/verification/profile/settings UI, email templates/service calls, auth/session/controller/E2E tests.
- **Database/configuration migration:** add bounded nullable pending-email and request-time columns. No new session table is needed. Apply any security-stamp-aware pending-2FA serialization change compatibly by rejecting old cache entries after deployment.
- **Backward-compatibility risks:** unverified registrants no longer enter the app immediately; clients expecting registration 200/auth cookies or profile-email update via the generic endpoint must change; password reset logs out every device; passwordless/provider-only recovery becomes more explicit. Existing active sessions remain valid until a defined security event; an optional one-time deployment revoke-all is not necessary for this defect and is deferred unless incident response requires it.
- **Rollback:** registration/email-change endpoints can be disabled while verification/login remains available. Additive pending columns remain harmless. Do not restore non-revoking logout/reset. If UI/API version skew occurs, reject email mutation safely and keep the old confirmed email.
- **Deployment sequence:** (1) P0-2A canonical links; (2) deploy P0-4A revocation helper and logout/reset/change behavior; (3) apply additive pending-email migration; (4) deploy P0-4B API/UI together with registration behavior; (5) verify email sink and copied-session tests; (6) monitor failed confirmation/recovery counts without addresses; (7) update support procedures before enabling self-service email change.
- **Documentation changes:** state diagram and API responses; registration/resend behavior; email-change notices; logout/password reset/change effects; 2FA-preserving recovery; passwordless recovery/support playbook.
- **Dependencies:** P0-2A for safe links; P0-4A before P0-4B; P0-1B uses the same recent-reauth/revocation rules. P0-6 deletion also uses revoke-all.

#### Required tests and acceptance criteria

- Unit-test state transitions, generic enumeration-resistant responses, latest pending email, token/email mismatch, expiry/replay, username preservation and the last-credential rule.
- Integration-test zero session rows/cookies after verified-required registration; verify then login; duplicate email; old/new email login before/after confirmation; all/current/other session counts; copied cookie after logout/reset; security-stamp and pending-2FA invalidation; 2FA/recovery codes; passwordless/provider-linked users.
- End-to-end test registration, resend, verification, pending/cancel/confirm email, logout in two tabs, reset with two sessions and recovery with 2FA using a local email sink/mocks only.
- **Acceptance:** an unverified mailbox never grants protected access or becomes the account's active email; old email remains usable until proof; logout invalidates its exact copied `sid`; successful reset/recovery invalidates all prior sessions/trusted devices without disabling 2FA; password change retains only a reissued current session; and legitimate recovery never depends on an unverified email/provider claim.

### JT-010 — attachment filesystem/database consistency

#### Revalidated finding

- **Final severity/confidence/classification:** **Medium / High / likely defect**. The failure windows are confirmed in code; an actual production orphan/missing file was not reproduced.
- **Confirmed execution path:** `JobTrackerApi/Controllers/AttachmentsController.cs:199-258` validates/writes batch files sequentially before all later files and before `SaveChanges`; a later invalid file or DB failure can leave files without rows. Rename at `:117-196` moves the physical random storage file before saving metadata, so DB failure can leave a stale path. Delete commits row/flag changes, then best-effort deletes at `:189` and swallows failure, leaving an orphan. Existing tests cover type/size/derived flags, not these failure boundaries.
- **Existing mitigations:** authenticated owner-scoped application lookup; random filenames; root path helpers; extension/size/quota checks; database transactions in some metadata operations; derived-flag recomputation. There is no durable cross-resource commit or reconciliation record.

#### Recoverable mutation design

- Add one concrete attachment file store plus a small JSON operation journal under the attachment root; do not add a general storage-provider interface. Journal writes use same-volume atomic replace, UUID operation IDs, normalized root-validated paths and no user content/secrets.
- **Upload:** validate the entire batch and quota before writing; stream each file into `.staging/{operationId}`; open a DB transaction, create rows with final random paths and recompute flags; persist the journal; atomically move staged files to final paths; commit; remove the journal. Any pre-commit failure rolls back rows and removes staged/final files. Restart reconciliation treats rows-plus-final-files as committed and removes the journal, or removes stage/final files when rows do not exist.
- **Rename:** update display `FileName`, purpose and derived flags in one DB transaction. Never move the randomized physical `FilePath`. Preserve/validate the actual extension so display rename cannot disguise content.
- **Delete:** persist a journal and atomically move the file to `.trash/{operationId}`; delete row/recompute flags in one transaction; commit; then purge trash. On restart, restore the file when the row still exists, or purge it when the row is gone. A missing source becomes an observable inconsistency, not a swallowed success.
- Run reconciliation on startup and periodically for stale journals/staging/trash. Log operation ID, stage and counts only. Refuse paths outside the configured root and symlink/reparse-point escape.

#### Implementation contract

- **Proposed design:** implement the minimal journaled saga above around existing controller actions and derived-flag logic. P0-6 reuses its invariants but owns a separate deletion-quarantine journal.
- **Affected files/components:** `AttachmentsController`, attachment path/storage helper(s), application startup/hosted reconciliation, attachment model/context only if an operation ID later proves necessary, filesystem settings, and controller/storage integration tests.
- **Database/configuration migration:** none in the preferred design; the durable journal is filesystem-based. Add only staging/trash retention/reconciliation settings if defaults cannot be constants.
- **Backward-compatibility risks:** rename no longer changes the physical random filename (not an external contract); locked/permission-denied files return retryable failure instead of false success; startup may discover pre-existing orphans that lack a journal and must report rather than guess.
- **Rollback:** drain/reconcile all journals before reverting. Staged/trash files remain recoverable by operation ID; never delete an unknown pre-existing orphan automatically. No schema rollback.
- **Deployment sequence:** (1) back up/manifest attachment rows and files; (2) deploy reconciler in report-only mode for pre-existing inconsistencies; (3) resolve only reviewed synthetic/test inconsistencies; (4) enable journaled upload/rename/delete; (5) inject safe failures locally; (6) monitor stale journal/missing/orphan counters; (7) let P0-6 depend on the documented invariants.
- **Documentation changes:** row/file invariants, staging/journal/trash layout, restart decisions, retention, report-only handling for legacy orphans and operator recovery procedure.
- **Dependencies:** coordinate journal/root validation with P0-6A/B. No dependency on identity or parser packages.

#### Required tests and acceptance criteria

- Integration tests inject: invalid second file, cancellation during copy, DB save/commit failure, move collision, locked file, delete/purge failure and process restart at each journal stage.
- Test exact quota/size boundaries, repeated/double submissions, idempotent reconciliation, traversal/symlink escape and two-user isolation.
- Assert rename changes only metadata, derived flags remain consistent, and logs/errors contain no file content or unsafe path.
- **Acceptance:** after every injected failure, state is either fully committed or represented by one retryable journal; committed rows always have the expected file; deleted rows have no untracked live file; owner isolation remains; and legacy unknown orphans are reported without destructive guessing.

#### Repository implementation record (2026-08-02)

SEC-008 implements the same durable state machine with `<final>.uploading` and `<final>.deleting` marker files instead of a separate JSON record. The generated collision-resistant final path is already the operation identity, so a second file and parser would duplicate state without improving recovery. Startup reconciles markers against durable rows, logs counts only, preserves unknown plain orphans and rejects unmanaged paths. Transaction cleanup now occurs only after confirmed rollback; uncertain commit outcomes retain the marker. Focused tests pass 20/20 and the full backend passes 525/525. Browser, production report-only inventory, MariaDB and executable symlink checks remain unverified; see `docs/verification/sec-008-attachment-consistency.md`.

### JT-009 — complete account export and deletion

#### Revalidated data inventory and finding

- **Final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. `JobTrackerApi/Controllers/UsersController.cs` admin deletion calls Identity `UserManager.DeleteAsync`, while `JobTrackerApi/Data/ApplicationDbContext.cs` shows that most domain roots have no FK to `AspNetUsers`; owned rows, files and credentials remain. `BackupController` exports a partial application-key-encrypted backup, and `ExportController` exports jobs only; neither is a complete user-readable portability export.
- **Confirmed execution path:** the admin Users API deletes only the `ApplicationUser` through Identity. Identity-owned claims/logins/tokens/roles follow their own relationships, but domain entities remain keyed by user IDs or indirectly through applications without an `AspNetUsers` cascade. `JobTrackerApi/Services/AppPaths.cs` locates attachment/CV/export roots; `AvatarStorage.cs` owns hashed avatar directories; CV generators write date/candidate-derived exports without a durable owner mapping. Provider connection rows retain encrypted tokens/credentials, queued CV runs remain discoverable from the database, and current caches/logs/backups have no per-user erase operation.
- **Direct/indirect database ownership:** Identity user fields/claims/logins/tokens/roles; companies, jobs and job applications; correspondence, job events, attachments, tailored CV drafts, interview prep/AI notes, AI interactions, checklist items, cover-letter versions and interview-prep items; Career Profile, versions, experiences, education, skills, projects, certifications and languages; CV variants/versions; CV upload artifacts/extraction runs; Gmail/Graph/IMAP connections and Gmail review decisions; per-user rules; recovery codes, trusted devices and user sessions. Global `RuleSettings` and `SystemEmailSettings` are not user-owned and must not be exported/deleted as such.
- **Files/documents:** attachment files under job IDs; CV artifacts under user IDs; avatars under hashed user directories; generated CV PDFs/DOCX under date/candidate-derived paths without an owner record; daily export files, including legacy filenames containing raw owner IDs; crashed `jobtracker-cv-pdf` temp files. Data-protection keys are global and never deleted per user.
- **Tokens/queues/cache/logs:** encrypted Gmail/Graph tokens and IMAP credentials; database CV runs plus in-memory queue; backend AI result caches (up to six hours), sidecar cache (one hour), OAuth state (15 minutes), pending 2FA (five minutes), browser local storage; rotated application logs; backups and external provider/model services. Current caches are not owner-invalidatable, and backup retention is not defined.
- **Existing mitigations:** many queries use owner filters/soft delete; connection secrets are encrypted; sessions/trusted devices are owner-keyed; artifacts have pruning; generated CVs have time cleanup; OAuth callbacks recheck state/user in some paths. These are not a complete export/deletion lifecycle.

#### Readable export design

- Add an authenticated, recent-reauthenticated, rate-limited export request that streams a ZIP through a bounded private temporary file. It contains:
  - `manifest.json`: schema version, generated time, category/file counts, SHA-256 checksums and missing/unavailable warnings;
  - readable JSON/CSV for safe account/profile fields; company/job/application/workspace data; correspondence/events; Career Profile current/version/children; CV variants/versions/extraction history; AI drafts/notes/interactions/usage; checklist/cover/interview data; settings; provider connection/sync metadata; and session/trusted-device metadata;
  - original owned attachment/CV artifact bytes and owned generated documents;
  - `README.txt` describing formats, exclusions, external-provider data and backup/log retention.
- Exclude password hash, security stamp, TOTP secret, recovery/token hashes, provider access/refresh tokens, IMAP password, data-protection keys, internal authorization secrets and other users/global settings. Export linked-provider identifiers only to the degree useful to the user, with tokens redacted.
- Before enabling deletion, make future generated CV/daily export paths owner-scoped using an opaque owner directory and UUID filename. Current date-only generated outputs cannot be attributed safely; because they are regenerable and unreferenced, purge them as a separately reviewed rollout operation after root verification rather than guessing ownership. Future generators receive owner ID explicitly.

#### Deletion lifecycle design

- Add `DeletionStatus`/`DeletionRequestedAtUtc` to the user and durable `AccountDeletionRequest` plus `AccountDeletionFile`/manifest records. The request survives user-row deletion, records stage/attempt/count/sanitized error, and never stores content or credentials.
- Both self-service and admin deletion call one coordinator. Require recent reauthentication and exact confirmation; prevent last-admin/unsafe self-admin deletion. Return 202 and immediately mark the account deletion-pending, block sign-in/new mutations, unpublish public CVs, revoke all sessions/trusted devices and stop/cancel new queued work.
- The coordinator is an idempotent staged saga:
  1. **Preflight:** enumerate every row/file by owner with `IgnoreQueryFilters`, verify paths stay under configured roots, record counts/checksums and acquire an in-process per-user mutation gate. Multi-instance distributed locking is deferred until multiple API replicas are supported.
  2. **Provider cleanup:** best-effort revoke Google consent/token before deleting encrypted rows; delete Microsoft Graph local tokens and provide Microsoft consent-removal instructions rather than request broad revoke scopes; delete IMAP credentials. Retry transient provider failures for a bounded window, then complete local deletion with an auditable warning rather than retain credentials indefinitely.
  3. **File quarantine:** atomically move all owned live files to `DeletionQuarantine/{requestId}` on the same volume and journal each path. If any required move fails, do not delete database rows; retry/restore idempotently.
  4. **Database transaction:** explicitly delete deepest children first: job-application workspace children; attachment metadata; CV variant versions/variants; Career children/versions/profile; extraction runs/artifacts; provider connections/decisions/user rules; applications, jobs then companies; recovery/trusted/session rows; Identity claims/logins/tokens/roles and user last. Assert pre/post counts. Do not depend on missing FKs or global query filters.
  5. **Final purge:** after DB commit, purge quarantine and owner caches/temp state. A purge failure remains a retryable stage; it never recreates rows. Before DB commit, a failure restores quarantined files. OAuth callbacks and workers recheck active-user state so stale work cannot recreate data.
- Record a minimal deletion tombstone on a restricted append-only store separate from the restored database. It needs only a pseudonymous/raw stable user key, request ID and completion time sufficient to replay deletions after an old backup restore. Retain at least as long as the longest backup. This is required because the current indefinite backup retention could resurrect deleted accounts.
- **Immediate versus retained:** live DB rows/files/tokens/sessions/caches are removed on saga completion; rotated logs age out under documented retention and should contain only pseudonymous IDs; immutable backups remain until scheduled expiry and are not edited in place; any restore must replay tombstones before readiness. External provider/legal retention is documented separately. Exact backup/log retention and lawful requirements require operator/legal decision and are not certified by this technical design.

#### Implementation contract

- **Proposed design:** P0-6A creates one inventory/export manifest and owner-scoped generated paths. P0-6B reuses that inventory for the deletion saga. Do not overhaul every FK or build a general workflow engine in Phase 0.
- **Affected files/components:** `ApplicationUser`, `ApplicationDbContext`, new deletion request/file entities and EF migration; admin users controller and new account export/deletion endpoints/service; all owner root repositories/controllers; session/trusted-device/provider services; CV queue/workers; attachment/CV/avatar/export path helpers; generated CV/daily export code; cache keys; OAuth callbacks; UI account settings/admin UI; backup/restore deployment scripts/runbooks; comprehensive integration/E2E tests.
- **Database/configuration migration:** additive lifecycle/request/file tables and user status/time fields with bounded indexes; owner-scoped path settings; separate restricted tombstone volume/key and explicit live/log/backup/quarantine retention settings. Do not remove data or add broad cascade FKs in the migration.
- **Backward-compatibility risks:** deletion becomes asynchronous; pending users lose access immediately; generated download paths change; provider revocation can fail independently; an application rollback after a completed deletion cannot restore purged live data; old backups require tombstone replay before service. Existing encrypted admin backup format remains a backup artifact, not relabeled as user export.
- **Rollback:** P0-6A export/path changes are additive; keep old-path read support only for a bounded transition and purge only regenerable reviewed files. P0-6B can be disabled before accepting requests. Once a request reaches DB commit/purge it is intentionally irreversible; rollback means finishing/reconciling the saga, not restoring user data. Preserve request/tombstone records across application rollback.
- **Deployment sequence:** (1) decide/log backup retention, tombstone custody and provider semantics; (2) deploy owner-scoped generated paths and inventory/export disabled; (3) validate a two-user export manifest and purge only verified regenerable legacy generated outputs; (4) apply additive deletion migration and mount restricted ledger/quarantine storage; (5) deploy coordinator dark, exercise one disposable synthetic user including restart/failure; (6) verify backup-restore tombstone replay; (7) enable user export; (8) enable admin deletion; (9) enable self-service deletion only after support/runbook readiness.
- **Documentation changes:** exact export category/schema/exclusions; deletion state and timing; provider revoke limitations; immediate/live versus log/backup/external retention; operator retry/reconcile and tombstone restore procedure; legal-review questions; user-facing confirmation/warnings.
- **Dependencies:** P0-4A revoke-all; P0-5 attachment invariants; P0-6A before P0-6B; P2-2 backup rehearsal is needed before production self-service enablement even if implementation code is complete. Parser cleanup from P0-3 must expose owner/run cancellation hooks.

#### Required tests and acceptance criteria

- Build two synthetic users, each with every direct/indirect entity, attachment/CV/avatar/generated file, provider state, session/trusted device, queued/interrupted run and cache entry. Export/delete User A and assert User B byte/row counts and access are unchanged.
- Validate manifest schema/counts/checksums, readable JSON/CSV, binary file inclusion, redaction list, missing-file warnings, exact boundary/stream cancellation and repeated export.
- Inject provider timeout, file move/purge failure, DB failure before/after commit, worker race, OAuth callback, restart at every stage and repeated deletion request. Assert idempotent convergence and sanitized audit records.
- Restore a disposable pre-deletion backup, replay the separate tombstone and assert the deleted account/data never becomes ready/accessible. Verify log/backup retention is reported, not falsely claimed as immediate erasure.
- End-to-end test self/admin confirmation, last-admin guard, immediate pending lockout, progress/final status and unavailable completed account using disposable local accounts only.
- **Acceptance:** readable export covers every documented owned category or explicitly warns/excludes it; secret material is absent; deletion removes all live owned DB rows/files/tokens/queued work/cache without affecting another user; every partial failure is retryable/auditable; provider limitations and backup retention are truthful; restored backups cannot resurrect a completed deletion.

### Independently reviewable Phase 0 work packages

#### P0-2A — canonical application origin and Host guard

- **Scope/findings:** JT-002 application boundary only: parse `App:PublicBaseUrl` once; remove every request-host fallback; derive Host filtering; secure-cookie decision; config/controller tests.
- **Dependencies:** confirmed production URL and internal health host names.
- **Acceptance/tests:** JT-002 application tests above; all generated links/callbacks canonical; hostile Host/forwarded headers rejected; Production fails fast; local/Test remain green.
- **Rollback:** supply the last valid origin or roll back binaries; never restore request-host fallback/wildcard host.
- **Documentation/config:** `.env.example`, app settings contract and deployment preflight. No DB/dependency migration.
- **Effort:** Small.
- **Deferred:** Compose/nginx/Traefik changes to P0-2B; multi-origin support.

#### P0-2B — production ingress, proxy and Compose alignment

- **Scope/findings:** JT-002 deployment boundary: explicit production/dev Compose files, close bound ports, exact Traefik contract, nginx forwarding and explicit known-proxy configuration.
- **Dependencies:** P0-2A canonical host and actual operator-supplied Traefik proxy/network values.
- **Acceptance/tests:** merged-config and two-hop proxy tests above; exact router works, unknown host/direct ports do not.
- **Rollback:** keep direct ports closed; restore last valid proxy network/config.
- **Documentation:** production/development Compose commands and ingress smoke/runbook.
- **Effort:** Small to Medium.
- **Deferred:** service mesh/CDN/multiple public origins.

#### P0-1A — Microsoft tenant and issuer trust policy

- **Scope/findings:** JT-001 validator/config/raw-bearer branch; no data migration or legacy assignment.
- **Dependencies:** supported tenant mode chosen and configured.
- **Acceptance/tests:** validator/account-mode tests above; invalid/mismatched tenant rejected; no raw alternate trust path.
- **Rollback:** disable Microsoft auth; never re-enable issuer-blind validation.
- **Documentation:** tenant-mode matrix and sign-in/Graph setting distinction.
- **Effort:** Small to Medium.
- **Deferred:** canonical row migration/relink to P0-1B.

#### P0-1B — canonical Microsoft links and legacy relinking

- **Scope/findings:** JT-001 additive (`tid`,`oid`) schema, composite lookup, explicit linking, inventory and temporary recovery ceremony.
- **Dependencies:** P0-2A, P0-1A and P0-4 recent reauthentication/email proof for the complete legacy flow.
- **Acceptance/tests:** migration/collision/relink/last-credential tests above; zero silent backfill/merge.
- **Rollback:** retain additive columns/legacy evidence; disable relink/exchange; keep established canonical links immutable.
- **Documentation:** user/support/operator transition guide.
- **Effort:** Medium.
- **Deferred:** legacy-column removal to a later approved migration; Graph permission redesign.

#### P0-4A — session revocation primitives and security-event policy

- **Scope/findings:** JT-008 current/other/all revocation, logout, password change/reset/admin reset, trusted devices, stamp-aware pending 2FA.
- **Dependencies:** existing session table/validator only.
- **Acceptance/tests:** copied-session and security-event matrix above; idempotent logout; no 2FA bypass.
- **Rollback:** retain revocation behavior; disable affected mutation endpoints if version skew occurs.
- **Documentation:** session effects/recovery support matrix.
- **Effort:** Small to Medium.
- **Deferred:** passkeys and deployment-wide revoke-all without an incident need.

#### P0-4B — verified registration and pending-email transition

- **Scope/findings:** JT-007/JT-008 registration response/session behavior, pending email schema/endpoints/UI and all-session revocation on confirmation.
- **Dependencies:** P0-2A and P0-4A; reuse P0-1 recent reauthentication rules.
- **Acceptance/tests:** full registration/email transition E2E above; old email retained until proof; no protected unverified session.
- **Rollback:** disable registration/email change; keep old confirmed email and additive fields.
- **Documentation:** API/UI state transitions and resend/recovery guidance.
- **Effort:** Medium.
- **Deferred:** passwordless credential creation and passkeys.

#### P0-3A — compatible parser dependency update

- **Scope/findings:** JT-006 exact parser dependency resolution, lock/hashes, advisory review and benign extraction corpus only.
- **Dependencies:** approved package-index/advisory access during implementation; final compatible FastAPI/Starlette pair.
- **Acceptance/tests:** no unaccepted reachable High/Critical advisory; clean locked install; benign corpus parity.
- **Rollback:** disable parsing rather than deploy the vulnerable image; retain prior digest only for diagnosis.
- **Documentation:** version/advisory/exception record.
- **Effort:** Medium.
- **Deferred:** Transformers/Torch upgrade absent demonstrated JT-006 reachability.

#### P0-3B — bounded isolated parser and safe backend failure

- **Scope/findings:** JT-006/JT-011 streaming/signature/format limits, subprocess deadline/rlimits, queue backpressure, sanitized failures and removal of binary/DOCX fallback.
- **Dependencies:** P0-3A and an approved benign boundary corpus.
- **Acceptance/tests:** generated boundary, harmless timeout, cleanup/outage tests above; no unbounded path remains.
- **Rollback:** disable import/extraction; preserve artifacts for retry.
- **Documentation:** supported limits/errors/retry and isolation boundary.
- **Effort:** Large.
- **Deferred:** multi-process throughput and separate parser service until measured.

#### P0-3C — parser container controls and cleanup

- **Scope/findings:** JT-006 non-root/read-only/capability/PID/tmpfs/resource/network controls and stale temp cleanup.
- **Dependencies:** measured parent ML plus parser memory from P0-3B.
- **Acceptance/tests:** container assertions and restart cleanup above; healthy corpus fits explicit budget.
- **Rollback:** conservative budget adjustment or disable parsing; never restore root/unbounded exposure in Production.
- **Documentation:** sizing and operator cleanup/runbook.
- **Effort:** Small to Medium.
- **Deferred:** orchestration platform/sandbox service.

#### P0-5 — recoverable attachment mutations

- **Scope/findings:** JT-010 journaled validate-stage-commit/promotion, metadata-only rename, trash delete and reconciliation.
- **Dependencies:** shared root/path conventions coordinated with P0-6.
- **Acceptance/tests:** all injected filesystem/DB/restart/two-user tests above.
- **Rollback:** reconcile/drain journals first; preserve unknown legacy orphans.
- **Documentation:** invariants, journal states and operator recovery.
- **Effort:** Medium.
- **Deferred:** object storage, deduplication and general storage abstraction.

#### P0-6A — owner inventory, generated-path ownership and readable export

- **Scope/findings:** JT-009 one authoritative inventory/manifest; readable ZIP; redactions; owner-scoped future generated documents; reviewed legacy generated-file purge.
- **Dependencies:** retention/exclusion decisions and P0-5 path conventions.
- **Acceptance/tests:** two-user manifest/checksum/redaction/stream tests above.
- **Rollback:** disable export; retain old-path read only for transition; regenerate reviewed legacy outputs.
- **Documentation:** schema/categories/exclusions and retention statement.
- **Effort:** Medium to Large.
- **Deferred:** cross-product portability and importing the export.

#### P0-6B — idempotent deletion lifecycle and restore tombstones

- **Scope/findings:** JT-009 pending-account gate, provider cleanup, file quarantine, ordered DB purge, retry/audit, tombstone replay and UI/admin flows.
- **Dependencies:** P0-4A, P0-5, P0-6A; backup-retention/tombstone decisions and P2-2 rehearsal before production enablement.
- **Acceptance/tests:** two-user full inventory, fault/restart/idempotence, provider and backup-restore tests above.
- **Rollback:** disable new requests; reconcile accepted requests forward. Completed purge is intentionally irreversible.
- **Documentation:** confirmation, stages, retry, provider/backup/legal limitations and restore runbook.
- **Effort:** Large.
- **Deferred:** general workflow engine, broad cascade-FK rewrite, multi-replica distributed coordinator and legal certification.

## Phase 1 — broken core workflows and production reliability

### P1-1 — Restore SQLite/MariaDB parity for Career and Application Workspace

- **Findings/scope:** JT-003; every affected `DateTimeOffset` order/compare path through shared services/controllers.
- **Dependencies:** choose bounded materialisation versus common UTC storage mapping; preserve provider behaviour.
- **Acceptance criteria:** CV variants/runs, AI history/usage and workspace return correct owner/empty/non-owner results on SQLite and MariaDB.
- **Tests:** fresh/seeded HTTP provider matrix with zero/one/many dates and month boundary/timezone cases.
- **Rollback:** service-level query changes are reversible; any storage migration requires verified down/restore plan.
- **Documentation:** supported provider/date mapping and local setup.
- **Effort:** Medium.
- **Deferred:** adding PostgreSQL.

### P1-2 — Remove ambiguous workspace routes

- **Findings/scope:** JT-004; timeline/interview canonical actions and response DTOs.
- **Dependencies:** inventory frontend/test/API consumers and select compatible contract.
- **Acceptance criteria:** exactly one action per verb/path; owner 200, non-owner 404, anonymous 401; UI panels load.
- **Tests:** route-table uniqueness plus HTTP and browser panel tests.
- **Rollback:** keep a temporary differently named compatibility route only if a real caller requires it; do not reintroduce ambiguity.
- **Documentation:** current application-workspace endpoint reference.
- **Effort:** Small to Medium.
- **Deferred:** redesigning workspace API.

### P1-3 — Establish explicit owner-aware worker execution

- **Findings/scope:** JT-005; rules, reminders, daily export and enrichment; structured outcomes/errors.
- **Dependencies:** P0-4 notification/session policy where relevant; P1-4/P0-3 for AI safety; P1-4 below for user preferences/AI consent before activating sends/calls.
- **Acceptance criteria:** each enabled worker processes correct owners once; disabled work stays off; failures visible; no cross-owner data/output; restart idempotent.
- **Tests:** two-owner hosted integration, no HttpContext, enable/disable, retry/restart, clock boundary, email/AI fakes.
- **Rollback:** per-worker kill switches default safe; staged rollout; preserve prior data/status for reversal where possible.
- **Documentation:** worker schedule, idempotency, privacy effects and operator diagnostics.
- **Effort:** Medium.
- **Deferred:** distributed queue/multi-replica scheduler until scale requires it.

### P1-4 — Make notification and AI privacy controls real before worker activation

- **Findings/scope:** JT-012/JT-022; persistent per-user notification channels, AI enable/recipient/data summary and server enforcement.
- **Dependencies:** product/privacy decision on defaults and providers; P1-3 worker owner context.
- **Acceptance criteria:** user opt-out suppresses sends/calls; settings persist across devices; provider/data categories displayed; module payloads limited to documented fields.
- **Tests:** mixed users/default migration, fake email/AI call capture, attachment inclusion, background enrichment disabled/enabled.
- **Rollback:** global email/AI kill switches; conservative default disabled during migration.
- **Documentation:** privacy explanation, provider matrix, settings semantics.
- **Effort:** Medium.
- **Deferred:** per-module provider marketplace and advanced consent receipts.

### P1-5 — Bound job-import and request-body reads before buffering

- **Findings/scope:** remaining JT-011 job import and API/sidecar request limits.
- **Dependencies:** standard bounded-stream helper or framework request-size configuration; no new dependency needed.
- **Acceptance criteria:** declared/chunked oversize aborts before allocating/download beyond limit; slow/cancelled streams terminate.
- **Tests:** exact boundary, content-length over, chunked over, slow stream, cancellation and timeout.
- **Rollback:** configurable conservative limit; no unbounded fallback.
- **Documentation:** import/upload limits and error messages.
- **Effort:** Small to Medium.
- **Deferred:** large-document support.

## Phase 2 — regression tests, observability and recovery

### P2-1 — Add boundary-focused CI gates

- **Findings/scope:** JT-014/JT-016; route uniqueness, SQLite HTTP journeys, worker context, two-user auth, Python tests/audit, `tsc --noEmit`, minimum accessibility checks.
- **Dependencies:** Phase 1 fixes so new tests can start green; decide advisory exception policy.
- **Acceptance criteria:** every confirmed High runtime defect has a failing-before/fixed-after test; all gates run on PR/main without file whitelists.
- **Tests:** the new integration/browser/worker/security tests themselves; CI clean-cache rehearsal.
- **Rollback:** flaky test may be quarantined only with owner/expiry/evidence; never silently omit whole suites.
- **Documentation:** one authoritative local/CI command matrix.
- **Effort:** Medium.
- **Deferred:** raw coverage targets and exhaustive browser matrix.

### P2-2 — Define and rehearse complete provider recovery

- **Findings/scope:** JT-013; SQLite/MariaDB DB, attachments/CV artifacts/exports, key ring, secrets/config, RPO/RTO, off-host retention.
- **Dependencies:** operator storage/backup destination and encryption/key custody decisions.
- **Acceptance criteria:** automated artifact manifest; isolated restore for both providers within RTO/RPO; protected tokens and file downloads work; evidence retained.
- **Tests:** scheduled integrity/count/file manifest and disposable restore after migrations.
- **Rollback:** preserve immutable pre-restore backup; documented abort/forward migration decision.
- **Documentation:** exact backup/restore/rotation/runbook and rehearsal log template.
- **Effort:** Large operational.
- **Deferred:** multi-region disaster recovery unless business requirements demand it.

### P2-3 — Add actionable worker/deployment observability

- **Findings/scope:** JT-005/JT-013/JT-021; structured worker run counts/durations/errors, backup age, provider health, alerting, correlation.
- **Dependencies:** P1-3 worker semantics and chosen monitoring destination.
- **Acceptance criteria:** failed/stale worker or backup produces an actionable alert; logs contain owner-safe IDs/counters, not content/tokens; health distinguishes readiness/dependencies.
- **Tests:** fake failures, stale backup, partial AI/mail outage, alert routing in non-production sink.
- **Rollback:** log/metric additions are non-breaking; alert thresholds versioned and suppressible.
- **Documentation:** dashboards, alerts, runbooks and sensitive-log policy.
- **Effort:** Medium.
- **Deferred:** full tracing platform if logs/metrics meet current scale.

### P2-4 — Harden build provenance and secret scanning

**Status (2026-08-30): partially complete.** Exact .NET SDK selection, NuGet locks, hash-locked Python dependencies, npm lock enforcement, advisory gates, a no-value tracked-secret scanner with synthetic canaries, and deterministic multi-ecosystem CycloneDX generation now run in CI. Immutable action/image/installer identifiers, retained SBOM artifacts, container/licence scanning and the unresolved model-stack advisories still require verified upstream metadata or unavailable scanner/Docker runtime. See `docs/verification/jt-017-dotnet-provenance.md`, `docs/security/supply-chain-policy.md` and V-192/V-210.

- **Findings/scope:** JT-017/JT-020; action/image/installer pinning, SDK/locks/hashes, SBOM, container/secret scan, archive fixtures.
- **Dependencies:** approved update cadence and scanner availability.
- **Acceptance criteria:** immutable CI dependencies; reproducible documented toolchain; scans block policy-defined severity; no live credential patterns.
- **Tests:** clean-cache build, intentional canary secret/advisory fixture, SBOM diff review.
- **Rollback:** update pins through reviewed commits; avoid history rewrite without separate approval/coordination.
- **Documentation:** provenance/advisory exception and secret-response policy.
- **Effort:** Medium.
- **Deferred:** enterprise signing/attestation service if not needed yet.

## Phase 3 — architecture and maintainability

### P3-1 — Reduce dual schema ownership incrementally

**Status (2026-08-30): complete.** All 49 model tables have an executable, disjoint creation-owner classification and are migration-owned. All 35 formerly startup-created tables—including authentication/Identity, email-provider connections, CV persistence/history, job-workspace AI notes, workflow documents, durable interview preparation, and the complete Career Profile aggregate—moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite chain proof, and generated MariaDB SQL. The reconciler now performs guarded compatibility repair only. See `docs/verification/jt-019-schema-ownership.md` and V-194–V-208.

- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
- **Acceptance criteria:** every schema mutation has one owner; fresh/upgrade/repair matrices pass on SQLite/MariaDB; startup does no undocumented DDL.
- **Tests:** representative historical snapshots, interrupted migration/restart and malformed-empty legacy cases.
- **Rollback:** additive migrations first, backup-gated deployment, retain scoped legacy repair until telemetry proves removable.
- **Documentation:** schema ownership map and migration runbook.
- **Effort:** Large.
- **Deferred:** wholesale ORM/database rewrite.

### P3-2 — Rebuild the current developer/operator documentation

**Status (2026-08-30): repository scope complete.** The frontend/root/deployment/architecture sources now match Next.js 16, React Router 7, .NET 9, locked installs, current ports, and the implemented SQLite/MariaDB provider matrix. Clean install, lint, full frontend/backend tests, production build/TypeScript and locked NuGet restores pass; see `docs/verification/jt-018-developer-documentation.md` and V-193. A separate clean-machine/production operator rehearsal remains deployment evidence, not an undocumented implementation gap.

- **Findings/scope:** JT-018 plus JT-016; frontend README, supported database matrix, architecture/API/env/setup/test/deploy source of truth.
- **Dependencies:** Phase 0–2 behaviour/config decisions to avoid documenting transient state.
- **Acceptance criteria:** unfamiliar developer follows docs from clean clone through build/tests/local start; no CRA/PostgreSQL/stale API claims.
- **Tests:** execute every documented command in CI or scheduled clean environment where practical.
- **Rollback:** documentation-only; retain historical docs under clearly marked archive.
- **Documentation:** this item is the documentation change.
- **Effort:** Medium.
- **Deferred:** generated public API portal unless consumers require it.

## Phase 4 — accessibility, performance and UX

### P4-1 — Fix key accessibility semantics and verify responsive layouts

- **Findings/scope:** JT-015; identified icon buttons, CV cards, public CV width, keyboard/focus/viewport review.
- **Dependencies:** restored browser-control/test capability; approved accessible naming text.
- **Acceptance criteria:** role/name and keyboard parity on core pages; logical focus; no 375/768 overflow; 1440 layout remains readable.
- **Tests:** RTL role/name, axe, Playwright keyboard and 375/768/1440 screenshots; manual focus/contrast/reduced-motion check.
- **Rollback:** semantic attributes/layout adjustments can be individually reverted; no API/data risk.
- **Documentation:** accessibility test checklist and known limitations.
- **Effort:** Medium.
- **Deferred:** formal WCAG certification until manual assistive-technology audit.

### P4-2 — Measure before optimising large-data/admin/mail paths

- **Findings/scope:** JT-021; users/roles N+1, inbox cap/pagination, provider sync, bundle/route transfer, memory/query counts.
- **Dependencies:** representative synthetic large dataset and browser performance tooling.
- **Acceptance criteria:** agreed p95/query/memory/transfer targets; only measured failures changed; results remain complete/paged.
- **Tests:** query-count fixture, pagination boundaries, route bundle budget and non-disruptive sync benchmark.
- **Rollback:** retain old API contract behind compatibility version if pagination changes; compare before/after.
- **Documentation:** measurement method and capacity assumptions.
- **Effort:** Medium.
- **Deferred:** caching, queues or horizontal scaling without evidence.

## Phase 5 — optional enhancements/hardening

### P5-1 — Refine public-CV PDF abuse controls

- **Findings/scope:** JT-023; cache generated public PDF and/or partition client/slug/global limits.
- **Dependencies:** observed abuse/cost and privacy-safe cache invalidation.
- **Acceptance criteria:** one client cannot deny all viewers; total PDF generation remains bounded.
- **Tests:** multi-client same-slug and invalidation/burst cases.
- **Rollback:** revert limiter/caching policy; public slug contract unchanged.
- **Documentation:** public rate/cache behaviour.
- **Effort:** Small.
- **Deferred:** CDN until traffic justifies it.

### P5-2 — Close DNS-rebinding TOCTOU if deployment threat requires it

- **Findings/scope:** JT-024; pin validated public IP/connected peer for HTTP and IMAP while retaining TLS host validation.
- **Dependencies:** deterministic resolver/connect support and CDN/multi-address requirements.
- **Acceptance criteria:** a changed private answer/peer is rejected without breaking valid IPv4/IPv6 failover.
- **Tests:** resolver changes, mixed public/private answers, TLS SNI/certificate and timeout cases.
- **Rollback:** feature/config switch to current strict literal checks if pinning breaks legitimate providers; document residual risk.
- **Documentation:** supported network-resolution behaviour.
- **Effort:** Medium.
- **Deferred:** general outbound proxy/egress firewall unless infrastructure adopts one.

### P5-3 — Sandbox authenticated CV preview if compatibility permits

- **Findings/scope:** JT-025; minimum iframe sandbox and hostile renderer regression corpus.
- **Dependencies:** verify links/fonts/print/export under sandbox.
- **Acceptance criteria:** preview remains functional; hostile markup cannot execute or access parent origin.
- **Tests:** sandbox attribute, script/URL payloads, preview/PDF parity.
- **Rollback:** revert individual sandbox flag only if renderer encoding tests remain and issue is documented.
- **Documentation:** preview trust boundary.
- **Effort:** Small.
- **Deferred:** separate preview origin unless renderer threat changes.

## Recommended first approval slice

Approve **P0-2A — canonical application origin and Host guard** as the first implementation package, with this exact boundary:

1. Parse existing `App:PublicBaseUrl` once and fail Production startup unless it is canonical HTTPS.
2. Replace every `Request.Scheme`/`Request.Host` fallback and older base-URL alias in auth, admin reset, Gmail, Microsoft Graph, billing and follow-up URL creation.
3. Derive and enforce the production application Host allowlist from that origin, retaining only explicit internal health hosts.
4. Derive Production secure-cookie behavior from the canonical origin rather than forwarded request input.
5. Make `APP_PUBLIC_BASE_URL` required in environment/deployment preflight and document the Development/Test localhost rule.
6. Add the hostile Host/forwarded-header, malformed-origin, URL-caller and local/Test regression tests specified under JT-002.

P0-2A has **no database migration and no dependency change**. It must not include Microsoft linking, email/session behavior, Compose/nginx/Traefik changes, or unrelated URL abstractions. Review and commit it independently. Its acceptance gate is that request headers cannot influence any generated external URL, unknown production Hosts are rejected, Production fails fast on an unsafe origin, and existing local tests remain green.

Immediately after P0-2A, implement P0-2B to close repo-defined direct port/proxy exposure before enabling the P0-1B legacy relink or P0-4B email-change flows. Do not activate the currently inert AI/email workers as part of Phase 0.
