# Drift Findings — Targeted Verification Re-run, 2026-05-14 **Status**: discovery complete; **doc revisions and test-spec re-issues PENDING** (next session). **Scope**: targeted re-verification of `Auth/JwtExtensions.cs`, `Program.cs`, `Infrastructure/*.cs`, `Services/*.cs`, `Database/DatabaseMigrator.cs`, `Middleware/ErrorHandlingMiddleware.cs`, `Controllers/FlightsController.cs`, `Controllers/AircraftsController.cs` against the corresponding `_docs/` artifacts. **Trigger**: while preparing autodev Step 4 (Code Testability Revision), ran a code-level cross-check that contradicts the `_docs/02_document/04_verification_log.md` § 3 "All flow claims reconcile" verdict for F5 (JWT) + F6 (Startup) + AC-9 (Authz). **Root cause** (likely): the prior verification did doc-vs-doc consistency checks for these areas instead of opening the actual `.cs` files. The docs are internally consistent describing a HS256+shared-secret+permissive-CORS+dev-fallback world that no longer exists in the code. **Decision (user, this turn)**: Option A — re-run /document Step 4 targeted at the drifted areas, then re-issue test-spec for the affected ACs. --- ## Drift NOT covered by the B-ticket rename mapping These are real findings. Each item is actual today-code state, NOT a "post-rename target". The `_docs/` and the test specs in `_docs/02_document/tests/` need updates to match. ### D-JWT — Auth/JwtExtensions.cs (MAJOR) | # | Aspect | Doc claim (AC-5.*, modules/auth.md, components/05_identity, architecture.md § 7) | Code today (`Auth/JwtExtensions.cs`) | |---|--------|----------------------------------------------------------------------------------|--------------------------------------| | J1 | Algorithm | HS256 (`SymmetricSecurityKey(UTF-8(JWT_SECRET))`) | **ECDSA-SHA256** (`ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]`) with JWKS keys | | J2 | Key material | Shared HMAC secret in `JWT_SECRET` env var | **JWKS retrieved from `admin`** via `ConfigurationManager` + `JwksRetriever` + `HttpDocumentRetriever { RequireHttps = true }` | | J3 | Env var contract | `JWT_SECRET` (single var) | **Three vars**: `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` (NO `JWT_SECRET`) | | J4 | `ValidateIssuer` | `false` | **`true`** + `ValidIssuer = ` | | J5 | `ValidateAudience` | `false` | **`true`** + `ValidAudience = ` | | J6 | `ClockSkew` | `1 minute` | **`30 seconds`** | | J7 | Pinned algorithms | not mentioned | **`ValidAlgorithms = [EcdsaSha256]`** (forces algo to prevent HS256-confusion attack) | | J8 | `RequireSignedTokens` / `RequireExpirationTime` | not explicitly mentioned | both `true` | | J9 | Coupling to `admin` | "Local validation; this service never calls back to `admin`" | **Calls `admin` for JWKS** at startup + on `ConfigurationManager` refresh schedule | | J10 | Rotation model | "Token signed with old `JWT_SECRET` → 401 across the entire device until coordinated re-deploy" | **JWKS rotation on `admin`** + auto-refresh; no coordinated re-deploy needed | | J11 | Dev fallback | `JWT_SECRET=development-secret-key-min-32-chars!!` if env unset (ADR-005 carry-forward) | **No fallback**; `ConfigurationResolver.ResolveRequiredOrThrow` throws at startup if any of `JWT_ISSUER`/`JWT_AUDIENCE`/`JWT_JWKS_URL` is unset | | J12 | Authz policies | Single `"FL"` policy; `"GPS"` is the post-B7 target (existing-code has both today) | **Today has both `"FL"` AND `"GPS"`** — matches what the verification log already says, kept here for completeness | **ACs / NFTs to revise**: AC-5.1, AC-5.2, AC-5.3, AC-5.4, AC-5.5, AC-5.6, AC-5.7, AC-5.9; NFT-SEC-01–09; NFT-RES-07; FT-N-08; results_report.md AC-5 entire group; environment.md JWT mock spec; test-data.md JWT mint section. ### D-CONFIG — Program.cs + Infrastructure/ConfigurationResolver.cs (MAJOR) | # | Aspect | Doc claim | Code today | |---|--------|-----------|------------| | C1 | Required env vars | Two: `DATABASE_URL`, `JWT_SECRET` (E1) | **Four**: `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` | | C2 | Configuration source order | `IConfiguration` → `Environment.GetEnvironmentVariable` → fallback | **Env var first** (`Environment.GetEnvironmentVariable`), then `IConfiguration` key (e.g. `Database:Url`, `Jwt:Issuer`), then **throw** (no fallback) | | C3 | Dev fallbacks for `JWT_SECRET` / `DATABASE_URL` | "ungated by `IsDevelopment()`; production deploy without env vars silently boots with the dev secret" (ADR-005 carry-forward) | **No fallbacks at all**; production deploy without env vars now throws `InvalidOperationException` at startup. ADR-005 is OBSOLETE for this aspect | | C4 | `Database:Url` config-key alternative | not mentioned in docs (env-only) | **Code reads `Database:Url`** as fallback to `DATABASE_URL` env var | | C5 | `Jwt:Issuer` / `Jwt:Audience` / `Jwt:JwksUrl` config-key alternatives | not mentioned | **Code reads each** as fallback to its env var | **ACs / NFTs to revise**: AC-6.1, AC-6.2, E1, E3, E4 (no shared secret anymore); NFT-RES-05 (still tests DB-down crash, but the failure mode is more direct now); environment.md "Test Execution" env var list; test-data.md env vars; results_report.md AC-6 group + E3 lock test. ### D-CORS — Infrastructure/CorsConfigurationValidator.cs (MAJOR) | # | Aspect | Doc claim (E9) | Code today | |---|--------|----------------|------------| | O1 | Permissive policy scope | "`AllowAnyOrigin / AllowAnyMethod / AllowAnyHeader` in **all** environments (assumed safe behind suite reverse proxy)" | **Conditionally** permissive: `EnsureSafeForEnvironment` THROWS in `Production` if `CorsConfig:AllowedOrigins` is empty AND `CorsConfig:AllowAnyOrigin != true`. Permissive only when explicit opt-in OR non-Production | | O2 | Config keys | not mentioned | New keys: **`CorsConfig:AllowedOrigins`** (string array) and **`CorsConfig:AllowAnyOrigin`** (bool) | | O3 | Warning behavior | not mentioned | Logs `PermissiveDefaultWarning` at startup when implicit-permissive applies (origins empty + AllowAnyOrigin=false + non-Production) | **ACs / NFTs to revise**: E9 restriction; results_report.md E9 lock test (currently doesn't exist; was deferred to follow-up); ADR-002 in architecture.md only if it discusses CORS. ### D-DBSCHEMA — Database/DatabaseMigrator.cs (SMALL) | # | Aspect | Doc claim (data_parameters.md § 3) | Code today | |---|--------|------------------------------------|------------| | S1 | `created_date` / `first_seen_at` / `last_seen_at` column type | `TIMESTAMPTZ` | **`TIMESTAMP`** (no timezone) — affects how DateTime kinds round-trip | | S2 | Foreign-key declarations | "logical FK ... no DB-level FK constraint declared in migrator" (data_parameters.md § 3.2 + § 3.3 note) | **`REFERENCES (id)` declared on every FK** in the migrator (`flights.aircraft_id`, `waypoints.flight_id`, `orthophotos.flight_id`, `gps_corrections.flight_id` + `gps_corrections.waypoint_id`, `map_objects.flight_id`) | | S3 | Default values | not detailed | **Migrator sets `DEFAULT 0` / `DEFAULT FALSE` / `DEFAULT NOW()` / `DEFAULT ''`** on most non-nullable columns | **ACs / NFTs to revise**: data_parameters.md § 3 schema tables (TIMESTAMPTZ → TIMESTAMP, add REFERENCES notes, add DEFAULT notes); AC-2.8 (TOCTOU on FK) — actually now PARTLY mitigated by DB-level FK (insert would fail at DB layer with PG error 23503, not just app-layer); modules/database.md § Internal Logic. ### D-FILTER — Services/AircraftService.cs + FlightService.cs (SMALL) | # | Aspect | Doc claim (AC-1.6) | Code today | |---|--------|---------------------|------------| | F1 | Vehicle (today: Aircraft) `name` filter case sensitivity | "**case-sensitive contains** on `Name`" (AC-1.6, data_parameters.md § 2.1) | **`a.Name.ToLower().Contains(query.Name.ToLower())`** — **case-INSENSITIVE** contains | | F2 | Mission (today: Flight) `name` filter case sensitivity | not specified in AC-2.3 | **case-INSENSITIVE** contains (same `ToLower().Contains(ToLower())` pattern) | | F3 | Mission list ordering | AC-2.3 doesn't specify | **`OrderByDescending(f => f.CreatedDate)`** — newest first | | F4 | Vehicle list ordering | AC-1.5 doesn't specify | **`OrderBy(a => a.Name)`** — alphabetical ASC | **ACs / NFTs to revise**: AC-1.6 (case-INSENSITIVE); FT-N-01 (current test asserts "case mismatch returns 0 rows" which is WRONG against today's code — case is ignored, so a `name=br` query against `BR-01` actually returns 1 row, not 0); add ordering specs to AC-1.5 and AC-2.3; FT-P-04 + FT-P-08 should assert ordering. ### D-WP-NEST-CHECK — Services/WaypointService.cs (TINY) | # | Aspect | Doc claim (AC-4.2) | Code today | |---|--------|---------------------|------------| | W1 | Parent-mission existence check | "Parent mission missing → 404" | Code's `CreateWaypoint` checks `db.Flights.AnyAsync(f => f.Id == flightId)` and throws `KeyNotFoundException` ✓; `UpdateWaypoint` and `DeleteWaypoint` use a composite WHERE `w.FlightId == flightId && w.Id == waypointId` and throw `KeyNotFoundException` if no match — meaning the test for "parent missing" returns 404 BUT the doc-implied "parent missing first, then waypoint missing" two-step check is collapsed into one. | **ACs / NFTs to revise**: minor — clarify in AC-4.2 that the check is "matching `(flightId, waypointId)` returns no row → 404", which collapses two error cases into one. ### D-VERIFICATION-LOG — `_docs/02_document/04_verification_log.md` (META) The verification log itself is wrong: - § 3 row F5 (JWT validation): says "JwtExtensions matches exactly" — **wrong**, see D-JWT above. - § 3 row F6 (Startup + migration): says "matches exactly" but the docs claim hardcoded fallbacks while code has `ResolveRequiredOrThrow` — **wrong**, see D-CONFIG. - § 4.1 D6 (modules/middleware.md correction): correctly identifies the camelCase envelope, ✓. - § 4.2 F3 (carry-forward Swagger / CORS unconditional): "CORS unconditional" is wrong — code is gated. Swagger is still unconditional ✓. **Action**: re-issue § 3 rows F5, F6 with the new evidence; demote § 4.2 F3 (CORS unconditional) into the corrected list. --- ## Recommended re-verification + revision plan (next session) ### Phase 1 — `/document` re-run in `task` mode, scope = drifted files Inputs: this drift findings report. Skills: `.cursor/skills/document/SKILL.md` in **Task mode**. Files to update (estimate 10–12 doc files): | File | Sections to revise | |------|---------------------| | `_docs/02_document/architecture.md` | § 7 Cross-cutting (auth subsection: ECDSA+JWKS+iss/aud), § 7 (CORS subsection: gated), ADR-005 (mark obsolete or rewrite "no dev fallback" + "Swagger still ungated"), ADR-002 (no change — wire shape unaffected) | | `_docs/02_document/components/05_identity/description.md` | full rewrite of "Mechanism" + "Caveats" (ECDSA, JWKS, iss/aud, calls admin) | | `_docs/02_document/components/07_host/description.md` | Program.cs section (ConfigurationResolver, CorsConfigurationValidator); ADR-005 cross-ref | | `_docs/02_document/modules/auth.md` | full rewrite | | `_docs/02_document/modules/program.md` | rewrite startup section: env var contract, no fallback, CORS gating | | `_docs/02_document/modules/database.md` | TIMESTAMP (not TIMESTAMPTZ), REFERENCES declared, DEFAULT clauses | | `_docs/02_document/data_model.md` | § 11 schema table column types + FK note | | `_docs/02_document/04_verification_log.md` | re-issue § 3 F5+F6 rows; correct § 4.2 F3 | | `_docs/02_document/state.json` | append `decomposition_revised` entry recording the verification re-run; update `last_updated` | | `_docs/00_problem/problem.md` | review for any auth-shape claims | | `_docs/00_problem/acceptance_criteria.md` | AC-1.6 (case), AC-1.5 + AC-2.3 (ordering), AC-5.1–5.7, AC-5.9, AC-9.1 (today both `FL`+`GPS`), AC-6.1, AC-6.2 | | `_docs/00_problem/restrictions.md` | E1 (4 env vars), E3 (no fallback today), E4 (no shared secret), E9 (gated CORS), S6 (Swagger still ungated ✓ — no change) | | `_docs/00_problem/security_approach.md` | JWT validation, CORS gating, no dev secret | | `_docs/00_problem/input_data/data_parameters.md` | § 1 env vars (4 vars now), § 3 schema (TIMESTAMP, REFERENCES, DEFAULT) | | `_docs/01_solution/solution.md` | per-component table for 05 + 07 | ### Phase 2 — `/test-spec` re-issue in scoped mode Inputs: revised docs from Phase 1. Skill: `.cursor/skills/test-spec/SKILL.md` in **cycle-update mode** (NOT full re-run; scoped to AC-5, AC-6.1/6.2, AC-9.1, AC-1.5, AC-1.6, AC-2.3, E1, E3, E4, E9 + the 4 NFT families that those ACs feed). Files to revise: | File | Scope | |------|-------| | `_docs/00_problem/input_data/expected_results/results_report.md` | re-issue AC-1 row 1.6, AC-5 entire group, AC-6 rows 6.1–6.2, AC-9 row 9.1, AC-1 ordering rows, AC-2 ordering rows; add E3+E9 lock rows | | `_docs/02_document/tests/environment.md` | replace "in-process JWT mint with HS256 shared secret" with **"in-process ECDSA keypair + ephemeral JWKS HTTP service mock"** (e.g. WireMock.NET serves `/.well-known/jwks.json`); add `JWT_ISSUER` + `JWT_AUDIENCE` + `JWT_JWKS_URL` env vars; remove `JWT_SECRET` | | `_docs/02_document/tests/test-data.md` | rewrite "External Dependency Mocks" — `admin` JWKS mock; rewrite Data Validation Rules JWT rows | | `_docs/02_document/tests/blackbox-tests.md` | revise FT-N-01 (case-insensitive); add ordering assertions to FT-P-04 + FT-P-08 | | `_docs/02_document/tests/security-tests.md` | full revision of NFT-SEC-01 through NFT-SEC-09 (ECDSA, iss/aud, JWKS rotation, missing JWT_ISSUER startup throw) | | `_docs/02_document/tests/resilience-tests.md` | revise NFT-RES-05 (`ResolveRequiredOrThrow` failure modes — add scenarios for missing each of the 4 env vars); revise NFT-RES-07 (JWKS rotation, not shared-secret rotation) | | `_docs/02_document/tests/traceability-matrix.md` | re-trace AC-5, AC-6, AC-9, AC-1.5, AC-1.6, AC-2.3, E-rows | | `docker-compose.test.yml` | replace `JWT_SECRET` with `JWT_ISSUER` + `JWT_AUDIENCE` + `JWT_JWKS_URL`; add a `jwks-mock` service (e.g. WireMock or a small Kestrel test server) | ### Phase 3 — Resume autodev Step 4 (Code Testability Revision) After Phase 1+2: re-enter `existing-code` Step 4 with the revised docs + test specs. The original Step 4 analysis result ("code is largely testable as-is") still holds — the JWT/CORS/Config drift didn't introduce hardcoded paths or singletons, it just made the code MORE testable than the docs described. Expected outcome: Step 4 → "all scenarios testable as-is" → Step 5 (Decompose Tests, **session boundary**). --- ## Cross-cutting acknowledgements - The B-ticket plan (B5–B12) is unaffected. None of the drift overlaps with the rename/GPS-Denied work — the JWT/CORS/Config evolution happened independently. - The `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` leftover stays as-is. - The suite docs (`../suite/_docs/00_roles_permissions.md`, `../suite/_docs/05_identity*`, etc.) likely have correlated drift on the JWT model. Out of scope for this repo's `/autodev`; flag at suite-level next time `/autodev` runs in the suite workspace.