feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
@@ -0,0 +1,31 @@
# AI-001 verification — durable AI queue and backpressure
Updated: 2026-08-03
Status: `IMPLEMENTED — NOT VERIFIED`.
## Implemented
- Reuses `UserOperations`, terminal notifications and owner APIs/UI from OPS-001A/B/C.
- Server-side Pro/AI/privacy admission before operation creation.
- Owner/global bounded capacity, stable idempotency and status URL, deadlines, attempt limits and five priority bands.
- Claims only registered AI task types; unknown tasks are left untouched.
- Explicit owner scope, live entitlement/privacy recheck, cancellation monitoring, lease heartbeat, timeout, retry jitter and permanent/retryable failure classification.
- Configurable worker concurrency (default one), capacity/deadline/timeout settings and a default-off deployment switch.
- No raw CV, email, job description or prompt field was added.
## Evidence
- Focused `AiOperationQueueTests|UserOperationStoreTests|OperationsControllerTests`: 17/17.
- Full backend: 581/581.
- Compose config and `git diff --check`: pass with expected unset optional variables/line-ending notices.
- Existing operation tests cover atomic duplicate creation/claim, owner isolation, lease recovery after restart, deadlines, cancellation, retry, transaction rollback and notifications.
## Remaining gates
- AI-003/004 must register real Strategy/CV handlers and return actual 202 responses; no generic create API was exposed because it would bypass task ownership/policy.
- AI-002 must add provider/model concurrency, circuit health, provider/reason/model recording, payload minimization and external fallback decisions.
- Browser refresh/double-click/cancel/retry must be repeated against each real producer.
- Worker remains off; MariaDB and production canary/restart/queue telemetry are unavailable.
- The process-local capacity gate assumes one backend replica. Add a database reservation only before multi-replica rollout.
@@ -0,0 +1,38 @@
# Application action verification matrix
Updated: 2026-08-02
This is the rolling action-level evidence index. `PASS (automated/runtime)` is not a browser or production claim.
| Area | Action | Automated/API result | Browser | Production | Evidence |
|---|---|---|---|---|---|
| Origin | reject unknown/malformed production host | PASS | BLOCKED | NOT RUN | `sec-001-canonical-origin.md` |
| Ingress | production host-port and forwarded-proxy contract | PASS (config) | N/A | NOT RUN | `sec-002-ingress-compose.md` |
| Microsoft | tenant/issuer validation | PASS | BLOCKED | NOT RUN | `sec-003-microsoft-tenant.md` |
| Microsoft | canonical link/relink/unlink isolation | PASS | BLOCKED | NOT RUN | `sec-004-microsoft-identity.md` |
| Sessions | logout/reset/recovery revocation | PASS | BLOCKED | NOT RUN | `sec-005a-session-revocation.md` |
| Email | register/verify/pending-change ownership | PASS | BLOCKED | NOT RUN | `sec-005b-email-ownership.md` |
| Career/API | SQLite variants/runs/usage/history/workspace | PASS | BLOCKED | NOT RUN | `core-001-sqlite-provider-parity.md` |
| Application workspace | timeline/interview board/generated brief routes and owner isolation | PASS | BLOCKED | NOT RUN | `core-002-route-uniqueness.md` |
| Attachments | upload/list/download | PASS | BLOCKED | NOT RUN | `sec-008-attachment-consistency.md` |
| Attachments | metadata rename/purpose/AI flag | PASS | BLOCKED | NOT RUN | `sec-008-attachment-consistency.md` |
| Attachments | delete/restart recovery/two-user denial | PASS | BLOCKED | NOT RUN | `sec-008-attachment-consistency.md` |
| Workers | two-owner rules, export, reminders and enrichment | PASS (real SQLite; fake email/AI) | BLOCKED pending notification UI | NOT RUN; switches off | `bg-001-tenant-workers.md` |
| Durable operations | idempotent create/claim/lease/retry/cancel/complete | PASS (real SQLite) | NOT APPLICABLE until API/UI slice | NOT RUN | `ops-001a-durable-operations.md` |
| Notifications | atomic terminal record, owner list/read/dismiss | PASS (real SQLite; forced rollback) | NOT APPLICABLE until API/UI slice | NOT RUN | `ops-001b-notifications.md` |
| Operations UI | owner list/detail/cancel/retry and persistent notification surface | PASS (two-user HTTP + components) | BLOCKED | NOT RUN | `ops-001c-operation-ui.md` |
| Entitlements | Free direct request to every explicit AI action | PASS (policy/route inventory) | BLOCKED | NOT RUN | `pol-001-free-pro-entitlements.md` |
| Entitlements | stale Pro claim after downgrade | PASS (live-role policy test) | BLOCKED | NOT RUN | `pol-001-free-pro-entitlements.md` |
| Entitlements | queued CV and enrichment worker recheck | PASS (fake AI; real SQLite worker scopes) | N/A | NOT RUN; workers off | `pol-001-free-pro-entitlements.md` |
| Entitlements | Free core job create/detail and deterministic match data | PASS (automated) | BLOCKED | NOT RUN | `pol-001-free-pro-entitlements.md` |
| Entitlements | Free locked AI Workspace/Career/CV Builder/job-assistance states | PASS (components) | BLOCKED | NOT RUN | `pol-001-free-pro-entitlements.md` |
| Entitlements | Pro/Admin AI admission | PASS (automated policy) | BLOCKED | NOT RUN | `pol-001-free-pro-entitlements.md` |
| AI evaluation | synthetic task/category/privacy/constraint fixture coverage | PASS (19 cases; validator) | N/A | N/A | `prod-002-ai-evaluation.md` |
| AI privacy | disable AI for a current Pro user | PASS (live database policy + worker tests) | BLOCKED | NOT RUN | `pol-002-ai-privacy.md` |
| AI privacy | external `/cv/*` without administrator gate or user consent | PASS — forced local in backend/sidecar tests | BLOCKED | NOT RUN | `pol-002-ai-privacy.md` |
| AI privacy | approved external route with synthetic payload | PASS (mocked transport only) | BLOCKED | NOT RUN | `pol-002-ai-privacy.md` |
| Durable AI | Pro admission, idempotent status URL and bounded capacity | PASS (real SQLite + synthetic subject IDs) | BLOCKED until real producer | NOT RUN; worker off | `ai-001-durable-ai-queue.md` |
| Durable AI | priority/task-filtered atomic claim and owner-scoped success | PASS (fake handler, real operation/notification state) | N/A | NOT RUN | `ai-001-durable-ai-queue.md` |
| Durable AI | retryable failure, downgrade recheck, lease/cancel/restart recovery | PASS (automated) | BLOCKED until real producer | NOT RUN | `ai-001-durable-ai-queue.md` |
Remaining product actions are `NOT STARTED` in the master plan and will be added as their work packages enter verification. Browser localhost is currently denied by administrator policy; production access is not documented/configured.
@@ -0,0 +1,36 @@
# BG-001 tenant-safe worker foundation verification
Updated: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. The owner-scoping foundation and default-off activation contract pass local tests. Browser, production canary, durable notification/idempotency and multi-replica gates remain.
## Revalidated root cause and scope
Rules, follow-up reminders, daily export and job enrichment created request-scoped `JobTrackerContext` instances without an HTTP user. Deny-on-null global filters therefore returned no owned rows. Rules swallowed every exception; the other loops appeared healthy while doing empty work. CV processing already uses explicit owner predicates on each unfiltered query and was not changed; backup and AI health probe are tenant-neutral.
`BackgroundTenantRunner` now performs the sole worker bypass: it enumerates distinct non-empty job owners with `IgnoreQueryFilters`, opens a new dependency-injection scope per owner, sets `CurrentUserService`, and then executes all work through the normal tenant filters. It processes owners sequentially, isolates failures, and logs only worker/failure categories and aggregate counts. It refuses to override any HTTP context.
The four repaired workers are deny-by-default through new switches. Old email/export settings alone cannot activate them. Real email, external AI and production services were not called.
## Automated evidence
| Check | Result |
|---|---|
| `dotnet test ... --filter "FullyQualifiedName~BackgroundWorkerTenantTests|FullyQualifiedName~CurrentUserIdLiveEvaluationTests|FullyQualifiedName~RulesEngineTests"` | PASS — 9/9 after final trust-boundary test |
| `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore` | PASS — 532/532 after the final trust-boundary test |
| `docker compose config --quiet` | PASS; only unset optional/local environment warnings |
| `git diff --check` | PASS — no whitespace errors; repository line-ending notices only |
Real SQLite tests prove two-owner/no-HTTP filtering, owner-failure isolation, per-owner rules with an idempotent second pass, atomic per-owner daily exports with hashed filenames, fake-AI enrichment for both owners, fake-email reminders for both confirmed owners, default-off behavior for all four workers and HTTP-context override refusal.
## Runtime evidence
An isolated app using disposable data under `docs/audits/evidence/bg-001-runtime` listened on `127.0.0.1:5306`. Health returned 200, no daily-export directory was created with all four default-off switches, and the exact `JobTrackerApi` PID 44980 was stopped; the port and process were then confirmed closed.
## Remaining gates and rollback
- Reminder delivery is not exactly-once across an email-success/database-failure boundary. Keep it off until OPS-001 supplies a persistent notification/outbox operation.
- AI enrichment must remain off until POL-001/POL-002 and durable AI operations are enforced server-side.
- Rules and export remain off pending notification/audit and retention/operator rollout respectively.
- No lease, heartbeat, distributed scheduler, restart/clock-boundary suite, browser surface or production canary was added here.
- Rollback is setting all four worker switches false, then reverting the runner/service changes. Do not delete export files or undo user-visible mutations without a separate reviewed procedure. No schema migration was introduced.
@@ -0,0 +1,50 @@
# CORE-001 — SQLite/provider parity verification
Date: 2026-08-02
## Result
`VERIFIED LOCALLY`. The audited SQLite `DateTimeOffset` failures are fixed without changing stored types or MariaDB query behavior. Production MariaDB execution and browser verification remain outstanding.
## Implementation
- SQLite materializes only owner/job-scoped CV variants, extraction runs/artifacts and AI rows before `DateTimeOffset` ordering or range comparison.
- MariaDB retains server-side ordering, range filtering, aggregation and pagination.
- The correction also covers CV retention cleanup and reprocess-artifact selection.
- No schema, dependency or deployment configuration changed.
## Automated evidence
- Focused affected-service suite: 81/81 passed.
- Real-provider compatibility suite: 3/3 passed in `SqliteDateTimeOffsetCompatibilityTests`.
- SQLite relational database: newest-first CV/history/workspace/assets, current-month/all-time AI usage, generation entitlement query, extraction runs, latest artifact and owner isolation.
- MariaDB/Pomelo: production ordering and range expressions generate SQL without opening a network connection.
- Full backend regression: 509/509 passed.
- `git diff --check`: passed (line-ending notices only).
- `docker compose config --quiet`: passed with expected unset optional-variable warnings.
## Runtime evidence
An isolated API used a disposable fresh SQLite root and synthetic `example.test` accounts only:
| Check | Result |
|---|---|
| `/health` | 200 |
| registration | 200; local cookies only |
| `/api/cv/variants` | 200 `[]` |
| `/api/profile-cv/runs` | 200 `[]` |
| `/api/ai/usage` | 200, zero usage |
| `/api/jobapplications/1/ai/history` | 200 `[]` |
| missing workspace | 404 |
| synthetic owner workspace | 200 with correct company/job/checklist aggregate |
| same workspace as User B | 404 |
| User B variants | 200 `[]` |
The exact isolated API process was stopped and port 5303 was confirmed closed. Logs and disposable evidence are under `docs/audits/evidence/core-001-runtime/`; no secret or personal data is present.
## Limitations
- Browser localhost access remains denied by the in-app browser administrator policy; no browser claim is made.
- No MariaDB server was available. Pomelo SQL generation passed, but execution awaits a disposable or production-safe MariaDB smoke.
- Direct `dotnet ef database update` against a completely blank SQLite file still fails in the pre-existing reconciler-owned schema gap at `AddJobEntityAndProspectStages`. The documented application startup path succeeds because the reconciler establishes those columns before migrations. This is JT-019 schema-ownership debt, not the JT-003 query defect, and historical migrations were not changed.
- Execution policy denied deletion of the exact disposable nested data directory; it is stopped and recorded in the session handoff.
@@ -0,0 +1,40 @@
# CORE-002 — application route uniqueness verification
Date: 2026-08-02
## Result
`IMPLEMENTED — NOT VERIFIED`. Backend route ambiguity and tenant behavior are verified locally. Browser and production checks remain.
## Contract
| Method/path | Single owner | Response purpose |
|---|---|---|
| `GET /api/jobapplications/{id}/timeline` | `ApplicationIntelligenceController` | grouped/filterable application timeline |
| `GET /api/jobapplications/{id}/interview-prep` | `InterviewPrepController` | editable durable interview-prep board |
| `GET /api/jobapplications/{id}/interview-prep/brief` | `JobApplicationsController` | cached generated brief with attachment context and explicit refresh |
The unused legacy flat timeline action/DTO were deleted. The generated brief moved because both interview representations are live and intentionally incompatible; neither was silently discarded or overloaded by query parameters.
## Evidence
- Reflection regression checks every public controller action and fails on duplicate normalized HTTP method/route pairs.
- Focused backend timeline/interview/route suite: 31/31 passed.
- Full backend: 511/511 passed.
- Focused frontend route/timeline/interview suites: 21/21 passed.
- Full frontend: 45 suites, 153/153 passed; production build passed.
- Isolated SQLite HTTP matrix using synthetic users:
| Path | Owner | Other user | Anonymous |
|---|---:|---:|---:|
| `/timeline` | 200 | 404 | 401 |
| `/interview-prep` | 200 | 404 | 401 |
| `/interview-prep/brief` | 200 | 404 | 401 |
The exact isolated API process was stopped and port 5304 was confirmed closed. Logs are under `docs/audits/evidence/core-001-runtime/core-002.*.log`.
## Limitations
- In-app browser localhost remains blocked by administrator policy, so direct/deep-link, Back/Forward and rendered-panel behavior is not claimed.
- No production deployment or MariaDB HTTP smoke was performed.
- The route move is an API contract change for undocumented direct consumers of the old generated-brief URL; repository callers and documentation are updated.
@@ -0,0 +1,28 @@
# OPS-001A durable operation verification
Updated: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. State, ownership, concurrency and SQLite migration checks pass. Executable MariaDB and production checks remain unavailable.
## Evidence
| Check | Result |
|---|---|
| `dotnet test ... --filter "FullyQualifiedName~UserOperationStoreTests"` | PASS — 7/7 real SQLite |
| Full backend suite | PASS — 539/539 |
| `dotnet ef migrations has-pending-model-changes ... --no-build` | PASS — no pending model changes |
| SQLite/MariaDB migration scripts from canonical-identity migration to `AddUserOperations` | PASS — create + unique/claim indexes; MariaDB uses eight `datetime(6)` fields and no `TEXT`/`longtext` |
| SQLite/MariaDB down scripts | PASS — both drop only `UserOperations` |
| Disposable existing SQLite upgrade, down, and re-upgrade | PASS |
| Fresh disposable application startup on 5307, followed by EF no-op update | PASS — health 200; database already current; exact PID stopped and port closed |
Tests cover owner-scoped idempotency, same key across owners, concurrent duplicate creation, concurrent claim exclusion, bounded lease recovery/final failure, running cancellation recovery, cross-owner mutation denial, retry delay, successful completion, terminal cancellation refusal, deadlines, input bounds and neutral/owner scope guards.
Generated evidence is under `docs/audits/evidence/ops-001a/`: `sqlite-up.sql`, `sqlite-down.sql`, `mariadb-up.sql`, `mariadb-down.sql`, disposable `upgrade.db`, and `fresh-runtime/`. All data is synthetic and local.
## Limitations and rollback
- MariaDB SQL was generated and inspected but not executed against a server.
- No handler, queue worker, notification, owner API, browser UI or production canary is active yet.
- No raw private payload field exists; later producers still require task-specific subject/policy validation.
- Stop producers/workers and drain/cancel rows before `Down`. The migration is additive on upgrade and drops only this new table on rollback.
@@ -0,0 +1,29 @@
# OPS-001B persistent notification verification
Date: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. Repository, real-SQLite and generated provider checks pass. MariaDB execution, owner API/UI/browser checks and production rollout remain.
## Verified locally
- Terminal success, permanent failure and cancellation each persist exactly one generic owner notification.
- The terminal operation mutation and notification insert share one relational transaction. A synthetic notification `SaveChanges` failure rolls the operation back to `running` and leaves no notification.
- Retryable failure creates no premature notification; duplicate completion/cancellation creates no duplicate.
- Lease-expiry failure/cancellation and queued-deadline failure use the same terminal transaction path.
- Owner query filters deny cross-user list/read/dismiss mutations. Unread, read and dismissed states behave consistently across file-backed SQLite contexts.
- Notifications contain bounded generic text and references only; no raw operation or private failure content is copied.
- The additive migration upgrades, downgrades and re-upgrades a disposable SQLite database. The model snapshot is current.
- Generated SQLite and MariaDB up/down scripts are under `docs/audits/evidence/ops-001b/`. MariaDB DDL uses bounded strings and `datetime(6)`; it was inspected but not executed.
## Commands and results
- `dotnet test ... --filter FullyQualifiedName~UserOperationStoreTests`: PASS — 9/9.
- `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore`: PASS — 541/541.
- `dotnet ef migrations script ...` for SQLite and MariaDB up/down: PASS after correcting the design-time MariaDB connection-string key; the invalid first output was overwritten.
- `dotnet ef database update` notification/up, operation/down, notification/up against disposable SQLite: PASS.
- `dotnet ef migrations has-pending-model-changes ... --no-build`: PASS — none.
- `git diff --check`: PASS — line-ending notices only.
## Limitations
No MariaDB server, production environment or browser localhost access is available. No email was sent and no worker was enabled. API/UI verification belongs to OPS-001C; feature handlers and production canaries remain later packages.
@@ -0,0 +1,35 @@
# OPS-001C operation API and UI verification
Date: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. API, real-SQLite HTTP, component and regression checks pass. In-app browser, MariaDB and production checks remain unavailable.
## Verified locally
- Local-auth owner APIs list/detail/cancel/retry operations and list/count/read/dismiss notifications.
- API DTOs omit idempotency keys, lease tokens, provider/model selection, private failure messages and result references.
- Invalid limits fail with 400; missing or cross-owner identifiers return 404; terminal conflicts return 409.
- Two disposable users against isolated SQLite each saw only their own operation. User A received 404 for User B's operation detail/cancel and notification read/dismiss; User A's own detail/cancel/read/dismiss/retry returned 200/204 as appropriate. Anonymous list returned 401.
- `/operations` renders loading, error, empty, progress, cancel, retry, read and dismiss states. A single busy guard prevents overlapping mutations.
- The shell bell uses persistent unread count and routes to `/operations`; reminder count remains independent. Bell and progress controls have accessible names.
- Polling is bounded to 15 seconds while the page is mounted and 60 seconds for the shell unread badge.
## Commands and results
- Focused operation controller/store tests: PASS — 12/12.
- Full backend suite: PASS — 544/544.
- Focused operations/shell component tests: PASS — 3/3.
- Full frontend suite: PASS — 47/47 suites, 156/156 tests.
- `npm run build`: PASS — Next.js compile and TypeScript.
- Isolated API on 5310 with two synthetic users and synthetic rows: PASS — owner and cross-owner matrix above; exact listener stopped.
## Corrected verification issues
- An npm regression command first ran from the repository root and failed because no root `package.json` exists; it was rerun from `job-tracker-ui` and passed.
- An initial runtime inherited the development connection string instead of `Data:Root`. Two exact synthetic accounts/sessions created there were removed, and a zero-count check passed. No unrelated local rows were changed.
- The isolated SQL seed initially used lowercase GUID text, which does not match EF's canonical SQLite GUID parameter representation. Those synthetic rows were replaced with uppercase GUID text before the passing HTTP matrix.
- The generated disposable Data Protection XML key was deleted and is not retained as evidence. The evidence database contains synthetic data only.
## Limitations
The browser administrator policy still denies localhost, so no real browser, responsive, theme, keyboard journey or screenshot is claimed. No MariaDB/production runtime, feature-specific operation producer, restart-during-active-work test or external provider was exercised. No worker or email delivery was enabled.
@@ -0,0 +1,60 @@
# POL-001 Free/Pro entitlement verification
Date: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. The server policy, worker rechecks, Free locked states and automated regressions pass. Real-browser, Stripe lifecycle, MariaDB and production checks remain unavailable.
## Canonical policy
- External plan names are `free` and `pro` only.
- Free retains core non-AI job tracking, deterministic match scoring, manual profile/CV editing, saved drafts, exports and existing AI history, but cannot start AI work.
- Pro and Admin use AI and Pro CV themes. The persisted Identity role remains `Premium`, and `Stripe:PricePremium` remains a compatibility key; neither is exposed as a public plan name.
- Current database roles are authoritative on every explicit HTTP AI action. A stale role claim cannot preserve access after downgrade.
- A locked explicit action returns HTTP 403 with `{ "code": "pro_required", "message": "This AI feature requires Pro." }`.
- Existing 250-call/1,000,000-token Pro ceilings remain because they are defined in the existing implementation roadmap. Free ceilings are zero. Only AI Workspace currently writes complete `AiInteraction` usage rows; this accounting gap blocks full verification and must be resolved as AI-001/AI-002 move all producers through durable operations.
## Entry-point inventory
| Capability | User entry / frontend | API or worker execution path | Admission and recheck | Usage accounting | Free behavior |
|---|---|---|---|---|---|
| AI Workspace modules | Job details → AI Workspace; `AiWorkspacePanel` | `POST /api/jobapplications/{jobId}/ai/generate``AiWorkspaceService``ISummarizerService` | `Pro` policy with live role lookup | `AiInteraction` call/token row; monthly check | Generate disabled; existing history/read/delete remain available |
| Candidate fit | Job details Candidate Fit and Strategy Snapshot | `GET .../{id}/candidate-fit` → attachment/correspondence context → multiple summarizer calls | `Pro` policy | No complete shared usage row | Deterministic `match-score` remains available; AI narrative locked |
| Focus plan | Job details Focus Plan and Strategy Snapshot | `GET .../{id}/focus-plan` → summarizer | `Pro` policy | No complete shared usage row | Locked; no synthetic fallback presented as generated |
| Interview brief | Job details Interview Prep | `GET .../{id}/interview-prep/brief` → summarizer | `Pro` policy | No complete shared usage row | Editable non-AI interview board remains available; generated brief locked |
| Tailored CV generation | Add Job option and job Tailored CV tab | `POST .../{id}/generate-tailored-cv-draft` → shared generation helpers → summarizer | `Pro` policy | No complete shared usage row | Job creation and manual tailored-draft editing remain available; no operation is started |
| Application package | Job workspace drafts | `POST .../{id}/generate-application-package` → attachment/email context → summarizer | `Pro` policy | No complete shared usage row | Existing/manual package drafts remain readable and editable |
| Follow-up draft | Job Follow-up tab | `GET .../{id}/followup-draft` → context → summarizer | `Pro` policy | No complete shared usage row | Manual correspondence data remains available; AI draft is locked |
| Job summary refresh | Job overview | `POST .../{id}/refresh-ai``SummarizeAsync` | `Pro` policy | No complete shared usage row | Existing summary/tags remain visible; refresh locked |
| Automatic job summary | Job create/detail | Core `POST /jobapplications` and `GET /{id}` optional summarizer calls | Live role condition inside core action | No complete shared usage row | Core request succeeds without calling AI |
| CV import/parse | Career Profile upload/parse/reprocess | `/profile-cv/upload`, `/parse`, `/reprocess` → extraction/structured parsing | `Pro` policy before admission; queued run rechecks live roles | CV-run state only | Manual profile editing and previous review runs remain available |
| CV rebuild/improve/rewrite/PDF | Career Profile AI buttons | `/rebuild`, `/improve`, `/rewrite-section`, `/rewrite-preview`, `/export-pdf` | `Pro` policy; queued rebuild/improve recheck live roles | CV-run state only | AI controls locked; manual profile data remains available |
| CV Builder writing aid | CV Builder AI Tools | `POST /api/cv/ai/assist` → summarizer | `Pro` policy | No complete shared usage row | AI buttons disabled; CV editing/history remain available |
| Pro CV themes | CV Builder Customize | `GET /api/cv/themes`; create/save validates selected theme | Live role lookup in theme catalog checks | Not applicable | Pro themes identified and unavailable; existing unchanged selection can still be saved |
| Job enrichment worker | No direct UI; disabled by default | `JobEnrichmentHostedService` per owner | Live role recheck immediately before summary; deterministic tag detection still runs for Free | No complete shared usage row | No model call; core tag enrichment remains possible |
| Admin AI probe | Admin system diagnostics | `/api/admin/system/ai/probe` | Admin role; Admin maps to Pro | Health metric only | Not a Free user path |
| Periodic service probe | No user entry | summarizer health probe | No private/user payload; operational health only | Health metric only | Not a user AI capability |
| Attachment storage | Add-job/files UI | `AttachmentsController` storage check | Central Free/Pro storage entitlement | Bytes stored | 250 MB Free; 5 GB Pro (existing defined capability) |
## Automated evidence
- `ProEntitlementAuthorizationTests`: Pro/Admin success, stale-claim downgrade failure, stable 403 body, and reflection inventory of all explicit AI actions.
- `BackgroundWorkerTenantTests`: Pro owners use fake AI; Free owners never call it.
- `ProfileCvControllerTests`: a queued CV run fails with `pro_required` semantics after downgrade and never reaches the model.
- `AccountPlansTests`: Free zero AI, Pro/Admin AI, and only `free`/`pro` external names.
- AI Workspace UI test: Free locked state, disabled generation and upgrade link.
- Full backend: 568/568.
- Full frontend: 47/47 suites, 157/157 tests.
- Production frontend build: pass.
- `git diff --check`: no whitespace errors; existing line-ending notices only.
## Limitations and remaining checks
- Browser localhost access is denied by the available browser policy, so 375/768/1440, keyboard, themes and actual navigation to the upgrade action are not claimed.
- Stripe webhook transitions were code-inspected and existing status tests cover active/trialing vs expired states, but no real or mocked end-to-end checkout/webhook cycle ran in this package.
- MariaDB and production were not changed or tested.
- Landing-page prices, a third “Bring your own key” tier and “Unlimited AI” claims remain assigned to PRODUCT-001; they are not presented as resolved by POL-001.
- Full cross-feature usage accounting is incomplete. It must be centralized with AI operation execution before provider rollout; current numeric ceilings must not be advertised as universal until then.
## Rollback
Revert the policy registrations, action attributes, worker checks and frontend plan context together. No schema or dependency change is involved. Keep workers disabled during rollback; reverting only the worker rechecks would restore a downgrade bypass.
+37
View File
@@ -0,0 +1,37 @@
# POL-002 verification — AI privacy and external consent
Updated: 2026-08-03
Status: `IMPLEMENTED — NOT VERIFIED`.
## Implemented
- Server-persisted per-user AI enable/disable and external-processing consent.
- Live AI authorization rejects a Pro user who disables AI with the stable `ai_disabled` reason.
- Optional job enrichment and queued CV processing recheck the live AI-enabled preference.
- External `/cv/*` processing requires administrator enablement, supported provider configuration, current Pro entitlement, AI enabled and explicit user consent.
- The sidecar independently rejects external selection unless its administrator gate and the backend permission header are both present.
- Settings UI explains local-only/default behaviour and cannot opt in while the deployment gate is unavailable.
- Provider credentials remain environment/server-only.
## Automated evidence
- Focused backend policy/entitlement/worker/CV tests: 72/72.
- Focused policy/header tests after final changes: 28/28.
- Sidecar tests: 18/18, including absent/present permission-header routing.
- Focused settings/entitlement UI tests: 8/8.
- Full backend: 576/576.
- Full frontend: 47/47 suites, 158/158 tests.
- Frontend production build: pass.
- EF pending-model check: pass; SQLite script adds `AiEnabled DEFAULT 1` and `ExternalAiProcessingAllowed DEFAULT 0`.
- `docker compose config --quiet`: pass with expected unset optional-environment warnings.
- `git diff --check`: pass; line-ending notices only.
## Unverified / remaining
- Browser verification is blocked by administrator policy.
- No external provider, paid service, production environment or real private data was used.
- MariaDB migration execution remains unavailable.
- Direct clean `dotnet ef database update` fails in the pre-existing historical SQLite migration chain before this migration (`AddJobEntityAndProspectStages` expects a reconciler-added column). The application startup reconciler path was not exercised because the local process-launch command was blocked by execution policy.
- Background operations do not yet carry a policy snapshot; they fail safe to local. AI-001/AI-002 own durable admission/recheck, actual-provider/reason recording, cost controls, payload minimization and bounded local-first fallback triggers.
@@ -0,0 +1,34 @@
# PROD-002 workload inventory and synthetic evaluation verification
Date: 2026-08-02
Status: `VERIFIED LOCALLY`. This package performs classification and fixture validation only; it does not benchmark or call a model.
## Delivered
- `docs/ai/workload-inventory.md` classifies every reachable AI or deterministic-adjacent task by input/size/output/schema/language/latency/quality/privacy/fallback/mode/determinism/current provider/Pro requirement.
- `JobTrackerApi.Tests/Fixtures/AiEvaluation/cases.json` contains 19 synthetic cases with constraint-based expected results.
- `AiEvaluationFixtureTests` proves required category/task coverage, unique IDs, bounded expanded input, `.invalid` contact domains, absence of the authorized private CV/path, strict-JSON assertions and prompt-injection refusal markers.
## Required coverage
English, Norwegian and mixed CVs; English/Norwegian/noisy/technology-heavy/sparse jobs; email classification; follow-up; Strategy Snapshot; CV tailoring; strict JSON; malformed document text; job/email prompt injection; long input; empty and invalid input.
## Results
- Fixture validation: 1/1 passed.
- Full backend regression: 569/569 passed.
- Full frontend regression: 47/47 suites, 157/157 tests.
- Frontend production build: passed.
- No provider, internet, production, paid service, personal document or email was accessed.
## Limits
- Latency bands are initial benchmark targets, not measurements.
- Provider/model values are repository defaults, not verified production state.
- Golden prose is intentionally omitted; later benchmark scoring must test factual constraints, schema, language, safety and useful content rather than exact wording.
- Model benchmarking and threshold decisions belong to PROD-003 after safe production/local hardware inventory.
## Rollback
Remove the inventory, synthetic fixture and its validator. No application, dependency, schema, provider or deployment state changed.
@@ -0,0 +1,36 @@
# SEC-001 canonical-origin verification
Date: 2026-08-02
Status: `VERIFIED LOCALLY`; proxy and production verification remain.
## Implemented boundary
- Production startup requires a clean HTTPS `App:PublicBaseUrl`; Development/Test defaults to `http://localhost:3000` when absent.
- Password-reset, verification, admin-reset, Gmail/Graph callback, billing and reminder URLs use that immutable origin.
- Production requests accept the canonical Host; `backend`, `localhost`, `127.0.0.1` and `::1` are accepted only for `/health`.
- Session, CSRF and trusted-device cookie security derives from the canonical origin, not request or forwarded headers.
- Deployment preflight requires the canonical HTTPS origin; legacy per-provider callback-origin variables were removed.
## Commands and results
| Command | Result |
|---|---|
| `dotnet build JobTrackerApi/JobTrackerApi.csproj -c Release --no-restore` | Pass; 0 warnings, 0 errors |
| focused `dotnet test` filter for origin/auth/Gmail/Graph/billing/2FA/session tests | Pass; 79/79 |
| `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore` | Pass; 474/474 |
| `docker compose config --quiet` with synthetic required values | Pass; only expected unset optional-variable warnings |
| `tr -d '\r' < deploy/deploy.sh \| bash -n` | Pass |
| `git show HEAD:deploy/deploy.sh \| tr -d '\r' \| bash -n` | Pass; confirms direct Git-Bash CRLF failure predates SEC-001 |
| `git diff --check` | Pass; line-ending conversion warnings only |
| trust-boundary `rg` for request Host/scheme, forwarded proto and legacy origin aliases | Pass; only the central production Host decision remains |
## Focused cases
`ExternalOriginTests` covers missing/blank/non-HTTPS production origins; credentials, path, query and fragment rejection; local default; canonical port matching; internal-health restriction; provider-override/host-poisoning resistance; and canonical secure-cookie behavior.
## Limitations and remaining checks
- A hidden local Production-mode process launch was rejected by the command policy before execution. No service or temporary database was created, and no runtime result is claimed.
- Complete reverse-proxy behavior belongs to SEC-002 and remains unverified.
- Reset/verification navigation needs a safe local email sink or mock plus browser runtime; no email was sent.
- Production canonical/hostile Host smoke is required before SEC-001 can be `DONE`.
@@ -0,0 +1,39 @@
# SEC-002 ingress and Compose verification
Date: 2026-08-02
Status: `VERIFIED LOCALLY`; operator Traefik and production checks remain.
## Implemented boundary
- Production automation explicitly selects `docker-compose.yml`; the auto-loaded override was replaced by explicitly selected `docker-compose.dev.yml`.
- Base Compose publishes no frontend, backend, ai-service, or bundled-Ollama host port. Development adds 3000, 5202, and profile-scoped 11434.
- Nginx and backend communicate over an internal dedicated CIDR; the backend accepts exactly one forwarded hop only from that CIDR.
- Nginx derives its application server name from `APP_PUBLIC_BASE_URL`, rejects other Hosts except `/health`, and preserves Traefik's replaced proto/client headers rather than substituting internal HTTP.
- Deploy preflight requires and validates the canonical origin and dedicated proxy CIDR. CI post-deploy commands use the production Compose file explicitly.
## Commands and results
| Command/check | Result |
|---|---|
| focused `ExternalOriginTests` including proxy config | Pass; 14/14 |
| full backend Release suite | Pass; 476/476 |
| frontend `npm run build` | Pass; Next production build and TypeScript |
| production and dev `docker compose ... config --quiet` | Pass |
| parsed Compose assertion including `bundled-ollama` | Pass; production ports absent; dev 3000/5202/11434; internal CIDR aligned |
| normalized `bash -n deploy/deploy.sh` | Pass |
| `bash -n job-tracker-ui/configure-nginx-origin.sh` | Pass |
| mounted nginx template `nginx -t` using already-installed local frontend image | Pass |
| ephemeral origin substitution with canonical host/port then `nginx -t` | Pass |
| ephemeral substitution with credential-bearing origin | Rejected as expected |
| `git diff --check` | Pass; line-ending conversion warnings only |
No image was pulled and no production or persistent service was changed. Ephemeral Docker validation containers were removed automatically.
## Limitations and production gates
- No Traefik configuration exists in this repository. Verify its exact `Host()` rule, TLS route, replacement of forwarding headers, selected Docker network, and hostile-Host rejection on the operator host.
- `WEB_PROXY_SUBNET` must be chosen after production Docker-network inventory; the example value is not a production fact.
- Host firewall and `docker ps`/published-port state are unverified.
- The exact `nginx:1.29.8-alpine` base image was not installed locally. Syntax was checked with the existing local nginx frontend image; approved CI must build the pinned Dockerfile.
- A complete local proxy/browser smoke was not run because rebuilding the pinned container would require an unavailable base image/package access. No browser claim is made.
- Rollback is a normal application-version rollback plus the previous `.env`; do not reintroduce the deleted auto-loaded override or published production ports. If the new CIDR overlaps, roll back before replacement and select a non-overlapping CIDR.
@@ -0,0 +1,35 @@
# SEC-003 Microsoft tenant validation verification
Date: 2026-08-02
Status: `VERIFIED LOCALLY`; JT-001 remains open pending SEC-004.
## Implemented boundary
- `Auth:MicrosoftTenant` supports an exact tenant GUID, `organizations`, `consumers`, or explicit `common`; Production requires a value when Microsoft sign-in is enabled.
- Tokens require GUID-shaped `tid` and `oid`, exact `https://login.microsoftonline.com/{tid}/v2.0` issuer agreement, allowed tenant mode, configured audience, valid signature and lifetime.
- The validator returns normalized tenant/object IDs and treats email-like claims as metadata (`EmailVerified=false`). `Subject` temporarily remains the normalized `oid` only for legacy controller compatibility until SEC-004.
- The undocumented raw Microsoft bearer scheme and smart-selector branch were removed. Microsoft identity tokens enter only through the exchange/link validator.
- The application sign-in tenant is explicitly separate from `Microsoft:TenantId` used for Graph mailbox OAuth.
## Commands and results
| Command/check | Result |
|---|---|
| focused Microsoft validator + auth controller tests | Pass; 42/42 |
| full backend Release suite | Pass; 491/491 |
| normalized deploy-shell syntax | Pass |
| production Compose config with synthetic `organizations` mode | Pass |
| source search for raw Microsoft bearer registration/selector | Removed; exchange validator is the remaining sign-in trust path |
The focused validator suite covers Production missing configuration, invalid mode, common,
organizations, consumers, exact single tenant, personal-account rejection/acceptance, missing and
non-GUID `tid`/`oid`, issuer/tenant mismatch, wrong audience, wrong signature, expiry, and identical
`oid` values in two tenants.
## Remaining risk and gates
- **JT-001 remains High / High / likely defect.** `ApplicationUser` still lacks canonical tenant/object columns and controller lookups still contain legacy subject/email behavior. This package validates identity input but does not claim safe account ownership.
- No Microsoft provider was contacted. Browser/provider success, cancellation, wrong-tenant and consent behavior are unverified.
- Production must inventory legacy link counts/collisions and alternate credentials before enabling the stricter policy.
- SEC-004 must add canonical pair persistence, collision-safe lookup and the approved legacy recovery ceremony. Until then Microsoft sign-in should remain disabled in production.
- Rollback can restore the prior validator binaries, but must not be used to re-enable raw bearer trust in production. No database change exists in this package.
@@ -0,0 +1,58 @@
# SEC-004 canonical Microsoft identity verification
Date: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. Repository implementation, synthetic controller tests,
frontend component tests, build, migration SQL and disposable SQLite upgrade checks pass. Real
Microsoft, browser, SMTP, MariaDB execution and production legacy inventory remain blocked.
## Implemented trust path
- Microsoft account ownership is the normalized GUID pair (`MicrosoftTenantId`,
`MicrosoftObjectId`) with a unique composite database index.
- Exchange and conflict lookup use only that pair. `MicrosoftSubject` is no longer written and
provider email is metadata only; a matching local email returns a safe conflict instead of a link.
- New Microsoft registration stores the pair immediately, leaves application email unconfirmed,
and obeys the verification-required 202/no-session gate.
- Explicit link requires the authenticated local user's current password. Link and unlink revoke
all sessions/trusted devices; passwordless unlink is refused as a last-credential guard.
- Legacy fields remain null-canonical evidence. A single eligible candidate receives a
purpose-bound proof at its confirmed application email. Confirmation requires that proof plus a
fresh Microsoft token for the exact same (`tid`, `oid`) pair. Multiple or unconfirmed candidates
require operator-assisted recovery.
- Frontend MSAL authority uses the same `AUTH_MICROSOFT_TENANT` value passed at build time.
## Evidence
- Focused auth controller tests: 34/34 passed.
- Full backend suite: 507/507 passed.
- Full frontend suite: 44 suites, 152/152 passed; production build passed.
- SQLite migration SQL uses two nullable `TEXT` columns and a unique composite index.
- MariaDB migration SQL uses two nullable `varchar(36)` columns and the same unique index.
- Disposable SQLite legacy rehearsal:
- two rows with duplicate legacy subject/email values remained unchanged and null-canonical;
- migration applied without backfill;
- first canonical pair assignment succeeded;
- duplicate pair assignment failed with SQLite unique-constraint exit 19.
- Production Compose interpolation includes the same tenant mode in backend configuration and the
frontend build argument.
## Blocked checks and residual risk
- No real Microsoft token/account was used. Issuer/tenant/signature behavior is covered by the
SEC-003 signed-token tests; exchange/link/recovery use mocked tenant-qualified principals.
- The existing browser policy blocker was not retried. No browser workflow, popup, accessibility,
mobile or screenshot claim is made.
- No SMTP sink was available, so the complete emailed recovery link was not exercised end to end.
- MariaDB SQL generation passed, but no disposable MariaDB execution environment was available.
- Production legacy counts and collision groups are unknown. Microsoft production enablement must
remain gated until the counts-only inventory in `deploy/README.md` and migration/version-skew
checks pass.
- Legacy rows with multiple candidates or unavailable/unconfirmed app email deliberately require
operator verification; no automated merge is attempted.
## Rollback
Disable Microsoft sign-in by clearing its client ID, roll back application binaries, and retain the
additive columns and legacy evidence. Do not restore subject-only or email auto-linking. The migration
`Down` is appropriate only before canonical pair data is relied upon.
@@ -0,0 +1,31 @@
# SEC-005A session and recovery revocation verification
Date: 2026-08-02
Status: `VERIFIED LOCALLY`; browser and production verification remain.
## Implemented transitions
- Logout is anonymous/idempotent, exempt from CSRF gating, best-effort reads valid or expired local session cookies, revokes only the matching `(userId, sid)` row, and always clears session/CSRF cookies.
- Local JWT validation now requires the principal user ID and `sid` to match the same live row.
- Successful password reset revokes every target session and trusted device while preserving 2FA configuration; reset mail is sent only for confirmed local-password accounts with a generic response otherwise.
- Successful password change revokes every old session, creates one replacement session, removes other trusted devices, and retains the current trusted device.
- Pending 2FA tokens issued by real sign-in flows carry the user's security stamp; password/reset stamp changes invalidate the pending challenge.
## Commands and results
| Command/check | Result |
|---|---|
| focused auth/session/2FA controller suite | Pass; 48/48 after adding the expired-cookie case |
| full backend Release suite | Pass; 497/497 |
| copied-principal validation after logout | Rejected as expected |
| reset with two target sessions/devices plus another user | Target revoked/removed; other user untouched; 2FA preserved |
| password change with two sessions and three device rows | Old sessions revoked; one new session; current device retained; other-user row untouched |
| pending 2FA with stale security stamp | Rejected and consumed |
## Limitations and rollback
- No real email was sent and no browser/multi-tab flow was run.
- Password change deliberately revokes before issuing the replacement. If replacement issuance fails, the password is changed and the user must sign in again; no old token remains valid.
- Existing pending tokens issued before deployment have no stamp and retain their five-minute lifetime for compatibility. All tokens issued after deployment are stamp-bound.
- Production rollout should verify copied-cookie invalidation, session-row counts, reset with 2FA and trusted-device behavior using disposable accounts.
- Rollback requires application binaries only; no schema changed. Do not restore non-revoking logout/reset behavior.
@@ -0,0 +1,61 @@
# SEC-005B email ownership verification
Date: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. Backend, frontend component, build, migration-script and isolated API runtime checks pass. Required real-browser, SMTP-link and production/MariaDB execution checks remain blocked.
## Implemented contract
- Registration with `Auth:RequireEmailVerification=true` returns HTTP 202 with `{ "verificationRequired": true }`, creates no `UserSession`, and emits no auth/CSRF cookie.
- Existing unconfirmed sessions are rejected by local session validation while verification is required.
- Generic profile updates no longer mutate `ApplicationUser.Email`.
- Local accounts request a new address with their current password. The active address remains unchanged and both current and proposed addresses receive non-secret notifications.
- `PendingEmail`, `PendingEmailRequestedAtUtc`, and a rotated security stamp make replacement requests invalidate older Identity change-email tokens.
- Confirmation accepts only the current pending address, uses `UserManager.ChangeEmailAsync`, updates username only when it still tracks the old email, clears pending state, and revokes all sessions/trusted devices.
- Cancellation requires the current password and clears pending state.
- ASP.NET Identity default token providers are registered; data-protection keys already persist under `Data:Root/keys`.
## Evidence
- Focused backend auth/revocation tests: 35/35 passed.
- Full backend suite: 501/501 passed.
- Full frontend suite: 43 suites, 151/151 tests passed.
- Frontend production build: passed.
- SQLite migration script: `PendingEmail TEXT`, `PendingEmailRequestedAtUtc TEXT`.
- MariaDB migration script: `PendingEmail varchar(320)`, `PendingEmailRequestedAtUtc datetime(6)`.
- Disposable SQLite upgrade rehearsal with earlier migrations marked applied: migration applied and both columns were present.
- Isolated API runtime, email disabled and synthetic address only:
- registration returned 202 and `verificationRequired=true`;
- no `Set-Cookie` header and zero client cookies;
- immediate login returned 403 `email_not_verified` and still zero cookies.
- No email was sent and no production service or database was contacted.
## Blocked or partial checks
- The in-app browser denied localhost because its admin policy check was unavailable. No workflow is labeled browser-tested; desktop/mobile/keyboard and visible confirmation checks remain.
- A fresh empty SQLite migration rehearsal failed in the pre-existing `AddJobEntityAndProspectStages` migration because `LastReminderEmailSentAt` is absent. SEC-005B's migration was not reached. The matching upgrade rehearsal passed; CORE-001 owns the broken fresh chain.
- MariaDB SQL generation passed, but no disposable MariaDB instance was available for execution.
- SMTP resend/request/confirm links and production version-skew remain unverified.
- The execution policy rejected cleanup of `C:\Users\Cesnimda\AppData\Local\Temp\jobtracker-sec005b-browser-20260802`. It contains only disposable synthetic runtime data and local data-protection material; no process is using it.
## Commands
```text
dotnet ef --version
dotnet ef migrations add AddPendingEmailChange --project JobTrackerApi/JobTrackerApi.csproj --startup-project JobTrackerApi/JobTrackerApi.csproj --no-build
dotnet build JobTrackerApi/JobTrackerApi.csproj --no-restore
dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --filter "FullyQualifiedName~AuthAndSystemControllerTests|FullyQualifiedName~AuthSessionRevocationTests"
dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore
npm run build
npm test -- --runInBand
dotnet ef migrations script 20260731115022_AddStripeBillingState 20260802205800_AddPendingEmailChange ...
dotnet ef database update ... (disposable SQLite fresh and upgrade rehearsals)
dotnet run --no-build --no-launch-profile --project JobTrackerApi/JobTrackerApi.csproj --urls http://127.0.0.1:5302
```
## Remaining acceptance checks
- Real-browser registration, resend, verification, email request, cancellation and confirmation using a local email sink.
- Expired/replayed real Identity token integration check and custom-username preservation integration check.
- Disposable MariaDB upgrade/rollback execution.
- Production SMTP/canonical-origin and rolling-version smoke with synthetic addresses.
@@ -0,0 +1,51 @@
# SEC-008 attachment consistency verification
Updated: 2026-08-02
Status: `IMPLEMENTED — NOT VERIFIED`. Repository, real-SQLite integration and isolated HTTP checks pass. Browser and production checks remain blocked.
## Revalidated execution path
The former upload path copied files before validating the full batch and before the database commit; rename moved bytes before metadata commit; delete committed metadata before a best-effort file delete. These were confirmed JT-010 split-brain windows. Existing mitigations were owner-scoped queries, generated storage names, size/type/quota validation and derived attachment flags; there was no durable recovery state.
The implemented invariant uses the generated final path as the operation identity:
- `<final>.uploading` is a durable staged upload. Startup promotes it when its database row exists and purges it when no row exists.
- `<final>.deleting` is quarantined deletion data. Startup restores it when its row exists and purges it when no row exists.
- plain unknown files are counted and preserved; missing rows/files and unsafe stored paths are counted for review.
- rename changes display metadata only.
- attachment paths must remain under the configured root without child symlink/junction traversal.
Transaction failure cleanup occurs only after a confirmed rollback. An uncertain commit/rollback outcome preserves the suffix marker so restart reconciliation, rather than an unsafe guess, decides from durable database state.
## Automated evidence
| Check | Result |
|---|---|
| `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --filter "FullyQualifiedName~AttachmentConsistencyTests|FullyQualifiedName~AttachmentFlagsRecomputeTests|FullyQualifiedName~AttachmentsControllerTests" --logger "console;verbosity=minimal"` | PASS — 20/20 |
| `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --logger "console;verbosity=minimal"` | PASS — 525/525 |
| `git diff --check` | PASS — no whitespace errors; only repository line-ending notices |
The real-SQLite tests cover invalid later files, cancelled copy, database rollback, upload-promotion failure and restart, delete-purge failure and restart, quarantined-delete restore, idempotent reconciliation, metadata-only rename with atomic flags, repeated same-name uploads, exact 10 MiB boundary, one-byte-over rejection, outside-root rejection, unknown-orphan preservation and two-user isolation. Failure injection uses no malicious documents.
## Runtime evidence
An isolated development API ran on `127.0.0.1:5305` with disposable synthetic SQLite data:
- User A login, `.txt` upload, list, download, metadata rename/purpose update and delete succeeded (`200/204` as applicable).
- User B received `404` for User A's attachment download and delete.
- the final owner list was empty after deletion.
- startup reconciliation completed before the service accepted requests.
- the exact API PID was stopped and port 5305 was confirmed closed.
Logs are `docs/audits/evidence/core-001-runtime/sec-008.stdout.log` and `sec-008.stderr.log`; the source fixture is `synthetic-attachment.txt`. They contain synthetic local data only.
## Unverified gates and rollback
- In-app browser access to localhost is administrator-policy blocked, so upload/rename/delete/refresh was not browser-tested.
- This Windows host denied creation of a disposable symbolic link; child reparse-point refusal was code-inspected but not executed. Outside-root traversal is tested.
- No production report-only orphan inventory, counter monitoring, multi-replica exercise or MariaDB runtime was performed.
- Periodic reconciliation is deliberately deferred; startup retry is the current recovery trigger.
- Entitlement quota behavior is owned by POL-001; the unchanged quota calculation was not reclassified as verified here.
Before rollback, reconcile or manually review all `.uploading` and `.deleting` markers. Never delete unknown plain orphans automatically. No database/configuration migration was introduced.