docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports

Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.

- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
  9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
  PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
  docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-17 17:04:32 +02:00
parent aa3567d8a8
commit b176a44627
275 changed files with 12554 additions and 0 deletions
@@ -0,0 +1,272 @@
# M013 Adversarial Security Assessment
## Scope
Tested as requested:
- Input validation issues
- Authentication flaws
- Authorization issues
- API security
- File upload vulnerabilities
- Data exposure
Assessment style: hostile, exploit-oriented, evidence-first.
## Confirmed Findings
### 1. Authenticated SSRF via hostname-based loopback bypass in job import preview
- **Category:** API security / input validation
- **Component:** `JobTrackerApi/Services/JobImport/JobImportService.cs`
- **Endpoint:** `POST /api/jobimport/preview`
- **Risk:** **High**
#### Vulnerability
`JobImportService.TryValidateUrl(...)` blocks literal loopback and private IPs, but it does **not** resolve hostnames before allowing the request. That means a hostname that resolves to a loopback/private address can bypass the protection.
The validator rejects:
- `http://127.0.0.1:5202/...`
- `http://[::1]:5202/...`
- `http://2130706433:5202/...`
But it accepted hostnames resolving to loopback, including:
- `http://127.0.0.1.nip.io:5202/api/auth/config`
- `http://localhost.localdomain:5202/api/auth/config`
#### Example exploit input
```http
POST /api/jobimport/preview
Authorization: Bearer <valid local token>
Content-Type: application/json
{
"url": "http://127.0.0.1.nip.io:5202/api/auth/config"
}
```
Observed result:
- request was **not** rejected as local/private
- server fetched the internal endpoint
- response progressed to parser failure (`No JobPosting schema found`), which is enough to prove the internal fetch happened
#### Why this matters
An authenticated attacker can use the server as an HTTP client against internal-only services or private network resources reachable from the API host. Depending on deployment, this can expose:
- internal admin/debug endpoints
- cloud metadata services
- internal service meshes
- localhost-only ports
- network topology and response behavior
#### Clear fix
- Resolve DNS before allowing the request.
- Reject any hostname whose resolved addresses are loopback, link-local, RFC1918 private, or otherwise internal.
- Re-resolve after redirects, or disable redirects entirely.
- Consider an allowlist of supported job domains instead of general outbound fetching.
- Log and rate-limit preview fetch attempts.
---
### 2. Subjectless signed local JWTs authenticate successfully and can disable owner scoping
- **Category:** Authentication flaws / authorization issues
- **Components:**
- `JobTrackerApi/Program.cs`
- `Data/JobTrackerContext.cs`
- several owner-scoped controllers relying on EF query filters
- **Risk:** **High**
#### Vulnerability
Local JWT validation accepts a correctly signed token **without** a required subject / nameidentifier claim.
Runtime proof:
- a signed local JWT with **no** `ClaimTypes.NameIdentifier` / `sub`
- but with valid issuer/audience/signature
- was accepted by the API
- `GET /api/auth/me` returned `200`
Observed response shape:
- provider: `external`
- id: `null`
- email echoed from token
At the same time, owner scoping in `Data/JobTrackerContext.cs` is defined like this:
```csharp
.HasQueryFilter(x => CurrentUserId == null || x.OwnerUserId == CurrentUserId)
```
If `CurrentUserId` is null, the filter collapses to **allow all rows** for owner-scoped entities.
That is a dangerous composition:
1. token is authenticated
2. current user id is null
3. owner filters disable themselves
4. endpoints that rely on implicit owner filtering become potentially cross-tenant
#### Example exploit input
A signed HS256 JWT using the app signing key, but **omitting** the nameidentifier claim.
Payload example:
```json
{
"iss": "JobTrackerApi",
"aud": "job-tracker-ui",
"nbf": 1775910345,
"exp": 1775913945,
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "ghost@example.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "ghost@example.com"
}
```
Observed runtime request:
```http
GET /api/auth/me
Authorization: Bearer <signed token without subject>
```
Observed result:
- `200 OK`
- request treated as authenticated even though no user identity key existed for owner scoping
#### Why this matters
If an attacker can forge or obtain a valid local signing key, this flaw is not just “login bypass” — it can become a **tenant-boundary bypass** because owner filters stop applying.
This is especially serious in environments where:
- the dev signing key is reused
- a staging or preview environment leaks the local JWT key
- operational mistakes deploy development auth settings
#### Clear fix
- Require a non-empty subject / nameidentifier claim during local JWT validation.
- Reject authenticated requests whose token does not map to a concrete application user identity.
- Change query filters so `CurrentUserId == null` means **deny**, not allow-all, for owner-scoped entities.
- Avoid relying on implicit global filters alone for sensitive raw-id endpoints; add explicit owner predicates in controller queries.
Example direction:
- validate a required identity claim in the JWT bearer events pipeline
- make owner filter logic equivalent to `CurrentUserId != null && x.OwnerUserId == CurrentUserId`
---
## High-Risk Candidates Not Fully Confirmed In This Runtime
These are not counted as confirmed findings yet, but they remain serious candidates.
### A. Raw-id child endpoints rely on implicit owner scoping
- **Category:** Authorization issues
- **Components:**
- `JobTrackerApi/Controllers/AttachmentsController.cs`
- `JobTrackerApi/Controllers/CorrespondenceController.cs`
- selected job-linked endpoints
- **Risk:** **Medium to High**
Patterns observed:
- existence checks like `AnyAsync(j => j.Id == jobId)`
- later child fetches through parent relationships
- reliance on EF global filters instead of explicit per-request owner predicates
If the owner filter is ever bypassed, weakened, or accidentally ignored, these become cross-user read/write primitives.
This runtime could not prove the full cross-user exploit path because the active SQLite file is missing core domain tables (`Companies`, `JobApplications`, `RuleSettings`) and those requests fail before authorization behavior can be fully exercised.
#### Suggested fix
Add explicit owner predicates in the endpoint queries themselves instead of trusting global filters as the only boundary.
---
## Tested Surfaces With No Confirmed Finding In This Pass
### Anonymous API reachability
Observed anonymous results in this runtime:
- `GET /api/export/jobs``401`
- `POST /api/backup/encrypted``401`
- `POST /api/jobimport/preview``401`
- `POST /api/client-errors``401`
This is better than the earlier pre-hardening posture.
### Gmail OAuth callback
- `GET /api/gmail/oauth/callback?code=fake&state=fake` returned a generic failure page
- no secret data exposure observed in this pass
### File upload path traversal via visible filename
Code review on:
- `AuthController.UploadAvatar`
- `AttachmentsController.Upload`
- `AttachmentsController.Rename`
- `ProfileCvController.Upload`
Current handling uses `Path.GetFileName(...)` and generated storage names, which is a reasonable defense against straightforward path traversal through user-supplied filenames.
No confirmed traversal exploit in this pass.
## Recommended Remediation Order
1. **Fix authenticated SSRF in job import preview**
- hostname resolution checks
- no internal/private destinations
- preferably domain allowlist
2. **Fix JWT subjectless-auth acceptance and owner-filter allow-all behavior**
- require subject/nameidentifier
- reject tokens that do not map to a real app identity
- change owner filters to deny on null current user
3. **Harden raw-id owner-sensitive endpoints with explicit owner predicates**
- attachments
- correspondence
- job-linked child endpoints
4. **Run a second exploit pass after the fixes**
- repeat cross-user probes
- retest SSRF bypasses
- fuzz upload/parser surfaces further
## Evidence Summary
### Runtime probes performed
- anonymous reachability checks against auth/config, csrf, auth/me, client-errors, jobimport preview, export, backup, and Gmail callback
- authenticated SSRF probes against job import preview using loopback-resolving hostnames
- authenticated malformed-token probe using a signed local JWT without subject/nameidentifier
### Key observed outputs
- `/api/jobimport/preview` accepted `http://127.0.0.1.nip.io:5202/api/auth/config`
- `/api/auth/me` returned `200` for a signed local JWT without subject/nameidentifier
- owner filters in `JobTrackerContext` explicitly allow all rows when `CurrentUserId == null`
## Honest Boundaries
- Some cross-user raw-id probes were limited by the local runtime using an incomplete SQLite schema.
- Those areas are reported as **high-risk candidates**, not falsely upgraded to confirmed findings.
- The two confirmed findings above are supported by direct runtime evidence plus code-path verification.
@@ -0,0 +1,123 @@
# M014 Security Remediation Verification
This report retests the two confirmed findings from `M013` after code fixes landed in `M014`.
Related assessment:
- `docs/security-assessments/M013-adversarial-security-assessment.md`
## Fixed Findings
### 1. Job import preview SSRF via hostname-based loopback/private-address bypass
- **Original issue:** `POST /api/jobimport/preview` accepted hostnames that resolved to loopback/private addresses and fetched internal targets.
- **Fix status:** **Fixed**
- **Primary code changes:**
- `JobTrackerApi/Services/JobImport/JobImportService.cs`
- `JobTrackerApi/Services/JobImport/IHostAddressResolver.cs`
- `JobTrackerApi/Program.cs`
#### What changed
- URL validation now resolves hostnames before allowing outbound fetches.
- Validation rejects loopback, private, link-local, and other internal destinations for both literal IPs and resolved hostnames.
- Automatic redirects are disabled on the `jobimport` HTTP client.
#### Retest inputs and outcomes
| Exploit input | Expected after fix | Observed |
| --- | --- | --- |
| `http://127.0.0.1.nip.io:5202/api/auth/config` | reject | `400` with parser `none` and local/private-network rejection |
| `http://localhost.localdomain:5202/api/auth/config` | reject | `400` with parser `none` and local/private-network rejection |
| `http://[::1]:5202/api/auth/config` | reject | `400` with local/private-network rejection |
| `http://2130706433:5202/api/auth/config` | reject | `400` with local/private-network rejection |
| `https://example.com` | allow public fetch path | request reached parser path and failed only with `No JobPosting schema found.` |
#### Verdict
**Pass.** The original SSRF exploit shapes are now blocked and a normal external URL still follows the intended public-host path.
---
### 2. Subjectless signed local JWTs authenticate successfully and can disable owner scoping
- **Original issue:** a validly signed local JWT without `nameidentifier` / `sub` was accepted, and owner filters were written to allow all rows when `CurrentUserId` was null.
- **Fix status:** **Fixed**
- **Primary code changes:**
- `JobTrackerApi/Services/LocalAuthIdentity.cs`
- `JobTrackerApi/Services/CurrentUserService.cs`
- `JobTrackerApi/Program.cs`
- `Data/JobTrackerContext.cs`
#### What changed
- Local JWT bearer validation now rejects tokens without a concrete subject/nameidentifier.
- Current-user resolution uses the same required-identity rule.
- Owner query filters now deny on null current user instead of allowing all rows.
#### Retest input and outcomes
Malformed token shape reused from `M013`:
- valid local signature
- valid issuer/audience/lifetime
- **missing** `ClaimTypes.NameIdentifier` / `sub`
Observed after fix:
| Request | Expected after fix | Observed |
| --- | --- | --- |
| `GET /api/auth/me` with subjectless signed local JWT | reject | `401` |
| `GET /api/companies` with subjectless signed local JWT | reject | `401` |
| `GET /api/rules` with subjectless signed local JWT | reject | `401` |
#### Automated proof
Focused tests now cover:
- required-subject local identity behavior
- fail-closed owner query filters when current user is missing
#### Verdict
**Pass.** The malformed token no longer authenticates, and the owner filters fail closed behind the auth boundary.
## Focused Test Evidence
### SSRF-focused tests
```bash
dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobImportServiceTests
```
Observed:
- passed
- covers loopback-resolving hostname rejection
- covers private-address hostname rejection
- covers normal public-host path
### Local-auth / owner-scope tests
```bash
dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter "LocalAuthIdentityTests|AuthAndSystemControllerTests|OwnershipGuardTests"
```
Observed:
- passed
- covers required-subject behavior and fail-closed owner filter semantics
## Remaining Boundaries
- The local runtime still uses a partial SQLite schema for some domain tables, so broader cross-user raw-id authorization retests remain best treated as a separate follow-up pass in a fuller environment.
- Those unresolved candidates were not needed to close the two confirmed `M013` findings, because both confirmed exploit shapes were retested directly and now fail.
## Final Verdict
`M014` closes the two confirmed `M013` vulnerabilities:
1. hostname-based authenticated SSRF in job import preview — **fixed**
2. subjectless local JWT authentication / owner-scope fail-open behavior — **fixed**
Both fixes were verified with focused automated tests and hostile runtime retests using the original exploit shapes.
@@ -0,0 +1,130 @@
# M015 Cross-User Authorization Replay Report
This report covers the follow-up tenant-boundary work after `M013` and `M014`.
Related artifacts:
- `docs/security-assessments/M013-adversarial-security-assessment.md`
- `docs/security-assessments/M014-security-remediation-verification.md`
- `docs/security-assessments/M015-hostile-fixture-setup.md`
- `docs/security-assessments/M015-hostile-fixture-setup.json`
- `docs/security-assessments/M015-s02-probe-results.json`
## Test Setup
A dedicated hostile-test SQLite database was created from the current EF model because the default development DB was missing core domain tables needed for real authorization probes.
Fixture runtime:
- clean SQLite DB under `.tmp/m015-fixture`
- API started with `Data__Root=/home/pi/development/JobTracker/.tmp/m015-fixture`
- registration temporarily enabled for the fixture runtime
- two real local users created through the API:
- `alice.m015@example.com`
- `bob.m015@example.com`
Alice-owned fixture resources created through the real API:
- `company_id = 1`
- `job_id = 1`
- `correspondence_id = 1`
- `attachment_id = 1`
All mutating requests used the real cookie + CSRF contract.
## Cross-User Probe Summary
Bob targeted Alices fixture ids with a real authenticated session.
### Defended in this pass
The following probes failed closed with `404` when Bob targeted Alices resources:
- `GET /api/attachments/1`
- `GET /api/attachments/download/1`
- `PATCH /api/attachments/1`
- `DELETE /api/attachments/1`
- `GET /api/correspondence/1`
- `DELETE /api/correspondence/1`
- `GET /api/jobapplications/1`
- `PUT /api/jobapplications/1`
- `PATCH /api/jobapplications/1/followup`
- `GET /api/jobapplications/1/timeline`
- `GET /api/jobapplications/1/tailored-cv-draft`
- `GET /api/jobapplications/1/followup-draft`
These routes did not expose or mutate Alice-owned data in this hostile fixture pass.
## Confirmed Finding
### Cross-user read leak on job history
- **Category:** Authorization / data exposure
- **Endpoint:** `GET /api/jobapplications/{id}/history`
- **Risk:** **Medium**
#### Vulnerability
Before the fix, Bob could request Alices job history by raw job id and receive Alices `JobEvent` rows.
Observed pre-fix response:
- `GET /api/jobapplications/1/history` as Bob
- `200 OK`
- payload included Alice-owned event data, including the `Created` event for Alices job
#### Example exploit input
```http
GET /api/jobapplications/1/history
Cookie: jobtracker_auth=<bob session cookie>
```
#### Root cause
Two issues combined:
1. `GetHistory(...)` queried `JobEvents` directly by `JobApplicationId` without verifying that the parent job belonged to the current user.
2. `JobEvent` had no owner-scoped query filter in `Data/JobTrackerContext.cs`.
#### Fix
- `GetHistory(...)` now checks whether the requested job exists in the current users scoped `JobApplications` query and returns `404` if it does not.
- `JobEvent` now has an owner-scoped query filter tied to `JobApplication.OwnerUserId`.
- Added focused regression test:
- `JobTrackerApi.Tests/JobApplicationsAuthorizationTests.cs`
#### Replay after fix
Observed post-fix response:
- `GET /api/jobapplications/1/history` as Bob
- `404 Not Found`
#### Verdict
**Fixed.**
## Automated Evidence
### Focused regression test
```bash
dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobApplicationsAuthorizationTests
```
Observed:
- passed
- verifies `GetHistory` returns `NotFound` for another users job
## Final Assessment
For the prioritized raw-id authorization seams exercised in this milestone:
- **confirmed and fixed:** `GET /api/jobapplications/{id}/history`
- **no finding in this fixture pass:** attachments, correspondence, primary job read/update, follow-up patch, timeline, tailored draft, follow-up draft
## Remaining Boundary
This report covers the endpoints actually exercised in the hostile fixture pass. It does **not** claim that every authorization-sensitive route in the application has been exhaustively proven safe; it closes the high-risk raw-id seams prioritized from the earlier assessment with a real two-user runtime and replay evidence.
@@ -0,0 +1,9 @@
{
"alice_email": "alice.m015@example.com",
"bob_email": "bob.m015@example.com",
"company_id": 1,
"job_id": 1,
"correspondence_id": 1,
"attachment_id": 1,
"api_base": "http://localhost:5202/api"
}
@@ -0,0 +1,47 @@
# M015 Hostile Fixture Setup
## Goal
Produce a trustworthy local runtime for cross-user authorization probes.
## Key discovery
The default development SQLite database in `JobTrackerApi/jobtracker.db` is **not** a trustworthy authorization-test target:
- it contains Identity and some later feature tables
- it does **not** contain the core domain tables needed for real cross-user job/correspondence/attachment probing
- current startup `Migrate()` behavior is therefore insufficient as the only hostile-test setup path
## Chosen fixture strategy
Use a dedicated clean SQLite fixture database created from the current EF model with `EnsureCreated()` semantics through a tiny helper program:
- helper project: `tools/hostile-fixture-db/`
- bootstrap script: `scripts/m015-hostile-fixture.sh`
This keeps the hostile runtime inside repo code and the real API host while avoiding ad-hoc manual SQL.
## What the helper does
- creates a clean `jobtracker.db` under a caller-provided data root
- builds the schema from the current `JobTrackerContext` model
- verifies the presence of core tables needed for M015:
- `Companies`
- `JobApplications`
- `Correspondences`
- `Attachments`
- `RuleSettings`
- `AspNetUsers`
## Runtime plan for S02
1. Run `scripts/m015-hostile-fixture.sh`.
2. Start the API with `Data__Root` pointing at that clean fixture root.
3. Mint an admin dev token against the fixture DB.
4. Create/reuse Alice and Bob through real API paths.
5. Seed Alice-owned company/job/correspondence/attachment fixtures through the real API.
6. Capture ids for cross-user hostile probes.
## Honest boundary
This slice establishes the trusted runtime path and fixture strategy. The full two-user seeded dataset and exploit execution belong in the next slice.
@@ -0,0 +1,158 @@
[
{
"method": "GET",
"path": "/attachments/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-2dad72a5823538bb6b92689d4569fe43-cf77b418a0f46da4-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/attachments/download/1",
"status": 404,
"preview": "",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "PATCH",
"path": "/attachments/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-d63b1ce08c89ba9ed4cedb01a7de8350-db6e76ebddea4885-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "DELETE",
"path": "/attachments/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-367415d13b5de0f54c2018849f526b04-df45573d91fb9c07-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/correspondence/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-df329ad92752ad3337e26fce98a92693-6cf9f400655056a8-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "DELETE",
"path": "/correspondence/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-286bd304cbc2b454c90fcd119b02aa47-f611583cbf3aacde-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/jobapplications/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-4cb171cc0edcfeee0f1b609c8feb8219-fb961ff6e8690e99-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "PUT",
"path": "/jobapplications/1",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-35f21fc1525cd7f0d397f1b92bdab7b7-f59dbe90203598dc-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "PATCH",
"path": "/jobapplications/1/followup",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-99cfcce88bf8e8fe44bb35f04ba88d48-122a6284b4914f60-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/jobapplications/1/history",
"status": 200,
"preview": "[{\"id\":1,\"type\":\"Created\",\"oldValue\":null,\"newValue\":null,\"note\":null,\"at\":\"2026-04-11T16:56:28.2097864\"}]",
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/jobapplications/1/timeline",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-1aa65bd46ff7617ccc1dfeae45c45dc5-d275b3da1ba9d2c5-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/jobapplications/1/tailored-cv-draft",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-6e28c94856419924997eda53896f0e6c-fcf5a60dd2947b9a-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
},
{
"method": "GET",
"path": "/jobapplications/1/followup-draft",
"status": 404,
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-f0ff37e63ea871aaeb34155de0d358bf-5f913a4b792c2e0d-00\"}",
"headers": {
"Content-Type": "application/problem+json; charset=utf-8",
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
"Server": "Kestrel",
"Transfer-Encoding": "chunked"
}
}
]