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,21 @@
# Accessibility code-review evidence
Captured: 2026-08-02
Classification: inspected in code and component tests only. Manual browser/assistive-technology verification was blocked by the missing in-app browser client.
## Confirmed code-level issues
- Icon-only controls lack programmatic names in `CompaniesTable.tsx:139,186`, `Correspondence.tsx:472`, `JobTable.tsx:493,704,745-748`, `SavedViewsMenu.tsx:93,153`, and `CvBuilderPage.tsx:96`. Tooltips or HTML `title` alone are not a reliable accessible name.
- Attachment preview/download/rename/delete buttons use `title` without `aria-label` at `Attachments.tsx:320-330`.
- CV cards are clickable `Paper` elements with pointer styling but no link/button role, `tabIndex`, or keyboard activation at `CvBuilderPage.tsx:91-98`.
- The public CV iframe is fixed at 210mm wide at `PublicCvPage.tsx:53-62`; at 375px and 768px this is a code-supported overflow risk, but visual clipping was not manually verified.
## Positive evidence
- Forms generally use MUI labels and many dialog close/action buttons have explicit `aria-label` values.
- CV editor move/hide controls and checklist controls carry accessible names.
- MUI supplies baseline focus and dialog semantics, though manual focus trapping/return was not checked.
- Public CV iframe has a title and sandbox attribute.
No axe, pa11y, or Lighthouse dependency/configuration is present. No automated accessibility gate runs in CI.
+10
View File
@@ -0,0 +1,10 @@
# AI-001 evidence index
- `../../../verification/ai-001-durable-ai-queue.md`
- `../../../architecture/durable-operations.md`
- `../../verification-log.md` entries V-096 and V-097
- `JobTrackerApi.Tests/AiOperationQueueTests.cs`
- Existing OPS evidence under sibling `ops-001a`, `ops-001b` and `ops-001c` directories
All execution used synthetic local SQLite data and fake handlers. No model, provider, browser or production service was invoked.
+29
View File
@@ -0,0 +1,29 @@
# Browser evidence and blocker
Captured: 2026-08-02
## Genuine running-browser coverage
The repository's own Playwright configuration created an isolated API, Next application, Chromium browser, and temporary database. `npm run test:e2e` passed 4/4:
1. local login establishes an authenticated session and reaches Dashboard;
2. a saved job is created through the reviewed multi-step UI;
3. the Career Workspace shell renders its heading and explanatory copy;
4. an explicitly published synthetic CV renders anonymously and its PDF response is a real `%PDF-` document.
The Career Workspace assertion proves only the shell rendered; it does not prove every downstream panel loaded. Later direct API checks confirmed that CV-list and application-workspace endpoints fail under default SQLite.
## Interactive browser blocker
The required `browser:control-in-app-browser` skill was selected for the broader manual journey, console, keyboard, responsive, and screenshot review. Its mandatory client module was absent from the installed plugin bundle:
`C:/Users/Cesnimda/.codex/plugins/cache/openai-bundled/browser/26.721.81911/scripts/browser-client.mjs`
Import through the required browser runtime failed with `Module not found`. The skill explicitly forbids substituting standalone Playwright or another browser-control implementation when its client is missing, so the interactive review stopped at that boundary.
## Consequences
- No audit screenshots were captured.
- Browser console/network monitoring beyond the passing repository tests was not performed.
- 375px, 768px, and 1440px viewport checks, keyboard-only navigation, modal focus, reduced motion, dark-theme comparison, back/forward, refresh, multi-tab, and throttled-network checks are blocked.
- Code inspection and component tests are labelled as such; they are not represented as manual browser testing.
@@ -0,0 +1 @@
Synthetic attachment for local SEC-008 verification only.
@@ -0,0 +1,30 @@
# Dependency and supply-chain evidence
Captured: 2026-08-02
## Advisory checks
- NuGet: no known vulnerable direct or transitive package was reported.
- npm: two affected package entries (`react-router` and `react-router-dom`) cover four moderate advisories. One narrow redirect issue is fixed in 6.30.4; the remaining audit-suggested resolution is React Router 7.18.2, a breaking major upgrade. No upgrade was attempted.
- Python: `pip-audit` returned 119 records across six installed packages; after deduplicating repeated aliases, the affected counts were `transformers` 21, `torch` 22, `pillow` 17, `pypdf` 35, `python-multipart` 6, and transitive `starlette` 7.
Reachability matters:
- `pypdf` directly parses authenticated user PDF uploads and multiple advisories describe infinite loops, excessive CPU, or memory exhaustion from crafted PDFs.
- Pillow directly opens authenticated image uploads; advisories include decompression bombs and memory-corruption cases. Extension routing is not a content-signature check.
- `python-multipart`/Starlette parse the sidecar upload before the endpoint's eight-megabyte post-read check; several advisories are request-parsing denial of service.
- Many `torch`/`transformers` advisories concern model or checkpoint loading. The application loads a fixed configured model, not a user-supplied model, so those records are not all treated as directly exploitable.
Existing mitigations: authenticated backend upload path, private backend-only AI network, required production service token, eight-megabyte application limit, accepted-extension list, and no host-published sidecar port. Residual risk: containers have no resource limits and the parser handles untrusted bytes in-process.
## Reproducibility and provenance
- npm has `package-lock.json` and uses `npm ci`.
- NuGet has no lockfile; the repository has no `global.json`, so local builds selected SDK 10 while the project targets .NET 9.
- Python top-level requirements are exact pins, but transitive dependencies are not hash-locked.
- Docker base images use mutable tags rather than digests.
- Gitea Actions use mutable major tags for checkout/setup-node, an unpinned remote `dotnet-install.sh`, and a tagged SSH action rather than immutable commit SHAs.
- The AI image upgrades pip/setuptools/wheel during build and downloads the configured Hugging Face model at runtime unless already cached.
- No SBOM generation, package licence gate, container CVE scan, or signed-provenance check is configured.
`dotnet list ... --deprecated` marked xUnit 2.9.2 and its transitive xUnit 2 packages as legacy. This is maintenance information, not a current security defect.
@@ -0,0 +1,8 @@
START TRANSACTION;
DROP TABLE `UserOperations`;
DELETE FROM `__EFMigrationsHistory`
WHERE `MigrationId` = '20260802224646_AddUserOperations';
COMMIT;
@@ -0,0 +1,42 @@
START TRANSACTION;
CREATE TABLE `UserOperations` (
`Id` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`OwnerUserId` varchar(255) NOT NULL,
`TaskType` varchar(64) NOT NULL,
`IdempotencyKey` varchar(128) NOT NULL,
`Status` varchar(32) NOT NULL,
`Priority` int NOT NULL,
`EntitlementDecision` varchar(32) NOT NULL,
`PrivacyPolicy` varchar(32) NOT NULL,
`SubjectType` varchar(64) NULL,
`SubjectId` varchar(128) NULL,
`Provider` varchar(128) NULL,
`Model` varchar(128) NULL,
`AttemptCount` int NOT NULL,
`MaxAttempts` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`AvailableAtUtc` datetime(6) NOT NULL,
`StartedAtUtc` datetime(6) NULL,
`CompletedAtUtc` datetime(6) NULL,
`DeadlineAtUtc` datetime(6) NULL,
`CancellationRequestedAtUtc` datetime(6) NULL,
`LeaseToken` char(32) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`LeaseExpiresAtUtc` datetime(6) NULL,
`LastHeartbeatAtUtc` datetime(6) NULL,
`ProgressStage` varchar(64) NULL,
`ProgressPercent` int NULL,
`FailureCategory` varchar(64) NULL,
`FailureMessage` varchar(512) NULL,
`ResultReference` varchar(256) NULL,
CONSTRAINT `PK_UserOperations` PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX `IX_UserOperations_OwnerUserId_TaskType_IdempotencyKey` ON `UserOperations` (`OwnerUserId`, `TaskType`, `IdempotencyKey`);
CREATE INDEX `IX_UserOperations_Status_AvailableAtUtc_Priority` ON `UserOperations` (`Status`, `AvailableAtUtc`, `Priority`);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20260802224646_AddUserOperations', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
DROP TABLE "UserOperations";
DELETE FROM "__EFMigrationsHistory"
WHERE "MigrationId" = '20260802224646_AddUserOperations';
COMMIT;
@@ -0,0 +1,41 @@
BEGIN TRANSACTION;
CREATE TABLE "UserOperations" (
"Id" TEXT NOT NULL CONSTRAINT "PK_UserOperations" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"TaskType" TEXT NOT NULL,
"IdempotencyKey" TEXT NOT NULL,
"Status" TEXT NOT NULL,
"Priority" INTEGER NOT NULL,
"EntitlementDecision" TEXT NOT NULL,
"PrivacyPolicy" TEXT NOT NULL,
"SubjectType" TEXT NULL,
"SubjectId" TEXT NULL,
"Provider" TEXT NULL,
"Model" TEXT NULL,
"AttemptCount" INTEGER NOT NULL,
"MaxAttempts" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"AvailableAtUtc" TEXT NOT NULL,
"StartedAtUtc" TEXT NULL,
"CompletedAtUtc" TEXT NULL,
"DeadlineAtUtc" TEXT NULL,
"CancellationRequestedAtUtc" TEXT NULL,
"LeaseToken" TEXT NULL,
"LeaseExpiresAtUtc" TEXT NULL,
"LastHeartbeatAtUtc" TEXT NULL,
"ProgressStage" TEXT NULL,
"ProgressPercent" INTEGER NULL,
"FailureCategory" TEXT NULL,
"FailureMessage" TEXT NULL,
"ResultReference" TEXT NULL
);
CREATE UNIQUE INDEX "IX_UserOperations_OwnerUserId_TaskType_IdempotencyKey" ON "UserOperations" ("OwnerUserId", "TaskType", "IdempotencyKey");
CREATE INDEX "IX_UserOperations_Status_AvailableAtUtc_Priority" ON "UserOperations" ("Status", "AvailableAtUtc", "Priority");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260802224646_AddUserOperations', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
START TRANSACTION;
DROP TABLE `UserNotifications`;
DELETE FROM `__EFMigrationsHistory`
WHERE `MigrationId` = '20260802225941_AddUserNotifications';
COMMIT;
@@ -0,0 +1,26 @@
START TRANSACTION;
CREATE TABLE `UserNotifications` (
`Id` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`OwnerUserId` varchar(255) NOT NULL,
`OperationId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`Kind` varchar(64) NOT NULL,
`Title` varchar(160) NOT NULL,
`Message` varchar(512) NOT NULL,
`LinkPath` varchar(256) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`ReadAtUtc` datetime(6) NULL,
`DismissedAtUtc` datetime(6) NULL,
CONSTRAINT `PK_UserNotifications` PRIMARY KEY (`Id`),
CONSTRAINT `FK_UserNotifications_UserOperations_OperationId`
FOREIGN KEY (`OperationId`) REFERENCES `UserOperations` (`Id`) ON DELETE SET NULL
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX `IX_UserNotifications_OperationId` ON `UserNotifications` (`OperationId`);
CREATE INDEX `IX_UserNotifications_OwnerUserId_DismissedAtUtc_ReadAtUtc_Create` ON `UserNotifications` (`OwnerUserId`, `DismissedAtUtc`, `ReadAtUtc`, `CreatedAtUtc`);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20260802225941_AddUserNotifications', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
DROP TABLE "UserNotifications";
DELETE FROM "__EFMigrationsHistory"
WHERE "MigrationId" = '20260802225941_AddUserNotifications';
COMMIT;
@@ -0,0 +1,24 @@
BEGIN TRANSACTION;
CREATE TABLE "UserNotifications" (
"Id" TEXT NOT NULL CONSTRAINT "PK_UserNotifications" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"OperationId" TEXT NULL,
"Kind" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Message" TEXT NOT NULL,
"LinkPath" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"ReadAtUtc" TEXT NULL,
"DismissedAtUtc" TEXT NULL,
CONSTRAINT "FK_UserNotifications_UserOperations_OperationId" FOREIGN KEY ("OperationId") REFERENCES "UserOperations" ("Id") ON DELETE SET NULL
);
CREATE UNIQUE INDEX "IX_UserNotifications_OperationId" ON "UserNotifications" ("OperationId");
CREATE INDEX "IX_UserNotifications_OwnerUserId_DismissedAtUtc_ReadAtUtc_CreatedAtUtc" ON "UserNotifications" ("OwnerUserId", "DismissedAtUtc", "ReadAtUtc", "CreatedAtUtc");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260802225941_AddUserNotifications', '9.0.14');
COMMIT;
+10
View File
@@ -0,0 +1,10 @@
# POL-001 evidence index
- Focused backend policy/worker/CV/controller slice: 74/74 passed.
- Full backend regression: 568/568 passed.
- Focused frontend AI/usage/job/CV slice: 22/22 passed.
- Full frontend regression: 47 suites and 157 tests passed.
- Frontend production build: passed.
- Patch hygiene: passed, with repository line-ending notices only.
No browser, provider, paid AI, email, production service or production data was used.
+10
View File
@@ -0,0 +1,10 @@
# POL-002 evidence index
- `../../../verification/pol-002-ai-privacy.md` — implementation and verification record.
- `../../../architecture/ai-privacy.md` — enforced configuration and trust-boundary contract.
- `../../verification-log.md` entries V-089 through V-095 — exact commands and outcomes.
- Synthetic-only provider routing tests: `tools/summarizer/tests/test_app.py`.
- Server policy tests: `JobTrackerApi.Tests/AiPrivacyPolicyTests.cs` and `ProEntitlementAuthorizationTests.cs`.
No screenshot was captured because localhost browser access is denied by administrator policy. No real CV, email, credential, provider call or production data was used.
+10
View File
@@ -0,0 +1,10 @@
# PROD-002 evidence index
- Synthetic fixture validation: 1/1 passed.
- Full backend: 569/569 passed.
- Full frontend: 47 suites, 157 tests passed.
- Frontend production build: passed.
- Model/provider calls: none.
- Real user data: none.
Primary evidence: `docs/verification/prod-002-ai-evaluation.md` and `docs/ai/workload-inventory.md`.
@@ -0,0 +1,60 @@
# Repository inventory evidence
Captured: 2026-08-02
## Scope and worktree
- Branch: `release-readiness`, tracking `origin/release-readiness`.
- Pre-existing user changes preserved: deleted tracked `.agent.md`; untracked `AGENTS.md`.
- Audit-created paths: `docs/audits/` only.
- Tracked-file distribution: 301 documentation files, 194 API files, 171 frontend files, 67 API-test files, 13 AI/tool files, 12 scripts, and 6 deployment files.
## Executable components
| Component | Implementation | Responsibility |
|---|---|---|
| Browser client | `job-tracker-ui/` — React 19, TypeScript, MUI, React Router inside a Next.js static-export shell | Public landing/auth pages and authenticated job, career, CV, email, settings, and administration workflows |
| API host | `JobTrackerApi/` — ASP.NET Core / .NET 9 | Authentication, authorization, REST endpoints, application workflows, data/file access, integrations, and hosted services |
| Data layer | EF Core 9; SQLite default or Pomelo MariaDB/MySQL | Identity and tenant-owned job, profile, CV, correspondence, attachment, AI, and workflow state |
| AI sidecar | `tools/summarizer/` — FastAPI, Transformers, OCR/document parsers | Local summaries and extraction; routes selected generation calls to Ollama, Gemini, or Groq |
| Background processing | Seven hosted services in the API process | Backups, rules, reminders, daily export, enrichment, AI readiness probing, and queued CV processing |
| Security fixture tool | `tools/hostile-fixture-db/` | Generates synthetic hostile database fixtures for local authorization testing |
| Delivery | Dockerfiles, Docker Compose, nginx, `deploy/deploy.sh`, Gitea Actions | Builds, health checks, backup-gated deployment, and direct-to-production replacement after CI |
## External boundaries
- Authentication: local ASP.NET Identity/JWT/cookie sessions; Google and Microsoft ID-token exchange/linking; TOTP 2FA.
- Mail: Gmail OAuth/API, Microsoft Graph, IMAP, and SMTP. Audit tests must mock these boundaries.
- Billing and abuse prevention: Stripe hosted flows/webhook and Cloudflare Turnstile.
- Job discovery/import: NAV feed and site-specific URL parsers for Finn, LinkedIn, and Jobbnorge; optional LibreTranslate.
- AI: private sidecar; Ollama locally or Gemini/Groq when configured.
- File/PDF: local data-root storage and headless Chromium PDF export.
## Data ownership and trust boundaries
- `ApplicationUser` is the identity root.
- Tenant entities use `OwnerUserId`; EF global query filters deny access when the current user is absent and scope reads to that owner.
- Several child entities rely on filtered parent navigation or explicit owner predicates rather than their own owner column.
- Public CV is the main anonymous data-release boundary and requires an explicit `IsPublic` flag plus a random slug.
- nginx is intended as the only production ingress to the API; the AI service is on a private backend-only network.
- The API process owns database migrations/reconciliation and all seven workers; the current deployment assumes one API replica.
## Documentation-to-code differences observed during discovery
- `job-tracker-ui/README.md` is obsolete Create React App boilerplate; the frontend now uses Next.js/Jest directly.
- `docs/architecture/current.md` reports a smaller/older controller surface and stale file sizes; the current controller directory contains 29 controller classes plus partials/DTO files.
- Root README API documentation omits substantial implemented surfaces including CV variants, billing, career profiles, AI workspace/history, job discovery, sessions, and several application-workspace APIs.
- `deploy/README.md` recommends PostgreSQL, but the application implements SQLite and MariaDB/MySQL providers only.
- Ignored local `vendor/`, `JobTrackerBackend/`, `.claude/worktrees/`, build outputs, databases, virtual environments, and frontend dependencies remain on disk but are not current tracked application source.
## Manual-audit exclusions
- Generated/build/runtime: `.next/`, `out/`, `build/`, `node_modules/`, `bin/`, `obj/`, local databases, backups, keys, CV artifacts, test results, caches, and virtual environments.
- Ignored historical/local copies: `.claude/worktrees/`, `JobTrackerBackend/`, `vendor/`, `tmp/`.
- Archived documentation under `docs/_archive/` is historical evidence, not the current implementation contract.
- Package lockfiles and EF generated migrations/model snapshot are reviewed for supply-chain and schema implications, not line-by-line as handwritten application logic.
## Unfinished-marker search
No production-code `TODO`, `FIXME`, `HACK`, stub, or `NotImplementedException` was found outside deliberate test doubles, normal placeholder UI text, and a guided-acceptance script template. This does not prove feature completeness; incomplete behaviour is assessed through routes, tests, and browser journeys.
+96
View File
@@ -0,0 +1,96 @@
# Disposable runtime evidence
Captured: 2026-08-02
Environment: local Development configuration, compiled Release API, Next development server, disposable SQLite data root under the current user's temporary directory. No production service, real mailbox, paid AI provider, or personal data was used.
## Default-provider endpoint results
Two synthetic local users were registered. User A owned one company, one job (`Syntetisk utvikler æøå`), one correspondence record, two CV variants, one career profile, and one text attachment. User B had no data.
| Request | User | Result | Evidence |
|---|---|---:|---|
| `GET /api/jobapplications?page=1&pageSize=20` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/companies` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/correspondence?jobApplicationId=1` | B | 200, 0 items | User A's message was not disclosed. |
| `GET /api/careerprofile` | B | 404 | User A's profile was not disclosed. |
| `GET /api/jobapplications/1` | A / B | 200 / 404 | Job ownership enforced. |
| `GET /api/attachments/1` | A / B | 200 / 404 | Attachment row and file ownership enforced. |
| `GET /api/cv/variants` | A and empty B | 500 | EF SQLite cannot translate the `DateTimeOffset` ordering. |
| `GET /api/jobapplications/1/workspace` | A | 500 | Same provider-translation family, reached on the core workspace. |
| `GET /api/jobapplications/1/ai/history` | A | 500 | Same provider-translation family. |
| `GET /api/ai/usage` | A | 500 | Same provider-translation family. |
| `GET /api/jobapplications/1/timeline` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/interview-prep` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/checklist` | A | 200 | Sibling workspace endpoint works. |
| `GET /api/jobapplications/1/analysis` | A | 200 | Sibling intelligence endpoint works. |
| `GET /api/jobapplications/1/match` | A | 200 | Sibling intelligence endpoint works. |
The application returned generic 500 problem responses; no secret-bearing exception details were exposed to the client.
## Background-service reproduction
The synthetic job was set to `Applied`, dated 40 days earlier than the audit, and had `Tags` and `ShortSummary` set to null. After a controlled API restart and more than the documented initial delays:
- status remained `Applied`, although the default rules threshold should have marked it ghosted;
- `Tags` remained null despite deterministic `SkillTagger` input containing `C#`;
- `ShortSummary` remained null.
This reproduces the deny-on-null tenant filter in a background scope with no HTTP user. The same query structure exists in rules, follow-up reminders, enrichment, and daily export. The queued CV processor is the counterexample: it deliberately uses `IgnoreQueryFilters()` for cross-tenant worker reads.
## Authentication lifecycle reproductions
### Logout
1. Logged in as User A and copied the issued synthetic cookie jar.
2. Posted logout with the CSRF cookie/header pair.
3. The browser jar received cookie deletion and subsequently got 401.
4. The copied pre-logout cookie still received 200 from `/api/auth/me`.
Result: logout clears client cookies but does not revoke the server-side `UserSession`.
### Email verification
A separate disposable API started with `Auth:RequireEmailVerification=true`, registration enabled, and email delivery disabled.
| Check | Result |
|---|---|
| Register synthetic account | 200 |
| Stored `EmailConfirmed` | `0` |
| Immediate `/api/auth/me` with registration-issued session | 200 |
The same synthetic row was then marked confirmed solely to isolate the email-change path. A normal login followed by `PUT /api/auth/profile` changed the email, returned 204, and left `EmailConfirmed=1`. No verification challenge was required for the new address.
## Backup and restoration rehearsal
The built-in SQLite backup runner created `jobtracker_backup_20260802_184234.db` from the disposable database. A read-only copied snapshot passed `PRAGMA integrity_check` and matched the current database:
| Dataset | Current | Restored copy |
|---|---:|---:|
| Users | 2 | 2 |
| Jobs | 1 | 1 |
| Companies | 1 | 1 |
| Attachment rows | 1 | 1 |
| CV variants | 2 | 2 |
A second isolated API was started from the restored database plus separately copied `Attachments/` and `keys/`. `/health` returned 200; synthetic login, job retrieval, and attachment download all returned 200.
Conclusion: SQLite database snapshotting works. Complete recovery additionally requires file storage, the Data Protection key ring, deployment secrets/configuration, and a documented restore procedure. MariaDB/MySQL backup is intentionally unsupported by the in-process runner and was not available for rehearsal.
## Safe local performance samples
Release API, disposable SQLite database with one job, ten requests after warm-up:
| Endpoint | Mean | p95 | Response bytes |
|---|---:|---:|---:|
| `/health` | 11.3 ms | 43.9 ms | 35 |
| `/api/jobapplications?page=1&pageSize=20` | 17.8 ms | 111.3 ms | 1,547 |
| `/api/companies` | 6.5 ms | 33.3 ms | 255 |
The static export contained 49 JavaScript chunks totalling 2,762,618 uncompressed bytes and one 293-byte CSS file. This aggregate is not an initial-page transfer measurement because Next.js loads route chunks selectively.
No load test was performed. Results do not establish production capacity.
## Cleanup
Listeners on audit ports 3001, 5402, 5403, and 5404 were resolved to their exact audit-owned command lines and stopped. Temporary evidence remains under the disposable local data root; no repository source/configuration was changed.
@@ -0,0 +1,40 @@
# Two-user isolation evidence
Captured: 2026-08-02
Scope: disposable local SQLite environment only. User A and User B used synthetic `@audit.invalid` identities. No production data or credentials were used.
## Results
| Resource or operation | UI level | API direct-ID test | Service/query protection | Result |
|---|---|---|---|---|
| Jobs | Browser blocked | A 200; B 404 for A's job; B list 0 | Explicit owner predicates and global `JobApplication` filter | Pass at API/data-query levels |
| Companies | Browser blocked | B 404 for A's company; B list 0 | Explicit owner predicates and global `Company` filter | Pass at API/data-query levels |
| Correspondence | Browser blocked | B list for A's job returned 0; copied ID returned 404 | Filter through owned `JobApplication` navigation | Pass at API/data-query levels |
| Attachments | Browser blocked | A upload/list/download 200; B job access and copied attachment download 404 | Owned-job query before file operation | Pass at API/data-query levels |
| Career Profile | Browser blocked | B's current-profile request 404 | Current-user lookup plus owner query filter | Pass at API/data-query levels |
| CV variants | Browser blocked | Copied A variant ID returned 404, but B's own list returned 500 | Owner predicate/filter exists; list blocked by SQLite translation | Partial: protection inspected and direct ID passed; list broken |
| Application workspace | Browser blocked | B copied A ID returned 404; A request returned 500 | Owner predicate exists; owner path blocked by SQLite translation | Partial |
| Checklist | Browser blocked | B copied A job returned 404; A 200 | Owner predicate on job/items | Pass at API/data-query levels |
| Timeline | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| Interview preparation | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| AI results | Browser blocked | History endpoint returned 500 | Owner predicates and query filter exist | Blocked by SQLite translation |
| Settings/session | Browser blocked | B `/auth/me` returned only B | Identity/session-bound current user | Pass at API level |
| Administrative operation | Browser blocked | B request returned 403 | `[Authorize(Roles = "Admin")]` | Pass at API level |
| Email threads/provider data | External providers not connected | Local correspondence passed; provider-specific IDs not live-tested | Owner-scoped connection and correspondence queries inspected | Code-inspected/partially tested |
## Protection layers observed
- UI: protected routes require an authenticated shell, but hiding was not counted as authorization.
- API: controllers use explicit local-auth or admin authorization attributes.
- Service: important reads carry `OwnerUserId` or owned-parent predicates.
- EF: global filters deny on null current user and match owner IDs.
- Database: tenant ownership is primarily enforced in application queries; many owner columns are not foreign keys to `AspNetUsers`, so database constraints alone do not provide tenant isolation.
## Limitations
- The in-app browser control client was missing, so UI navigation as A/B was not performed.
- Timeline/interview and several CV/AI paths failed before an ownership result could be observed.
- No Gmail, Microsoft Graph, IMAP, Stripe, cloud AI, or remote object storage was contacted.
No cross-user disclosure was confirmed in the paths that returned a meaningful result.