[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
ci/woodpecker/push/build-arm Pipeline failed

Security audit (5 phases) → reports under _docs/05_security/.

AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key
from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via
new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static
deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts.

AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via
package.json overrides in both roots; clean reinstall clears all
bun audit advisories.

Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44,
NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report.

Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7).

229 PASS / 13 SKIP / 0 FAIL on static + fast suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 05:31:11 +03:00
parent b016fd8207
commit f7dd6c98d8
32 changed files with 1833 additions and 502 deletions
+9 -9
View File
@@ -123,8 +123,8 @@ contract beautifully and accessibly".
|----------------------|------------------------|
| React 19 SPA (`src/`) | Suite backend services (`annotations/`, `flights/`, `admin/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`) |
| `mission-planner/` port-source (NOT deployed) | Database (PostgreSQL — managed by individual suite services, not the UI) |
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed directly by the SPA — security finding) |
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Map tile providers (OpenStreetMap, satellite tile URL via env) |
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed by the main SPA via env-resolved key per AZ-448 / AZ-449; consumed by `mission-planner/` per AZ-499 — env-resolved key, fail-soft on unset, manual revocation of the previously-committed key tracked under AC-42) |
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Suite-internal `satellite-provider` service for map tiles (same-origin via nginx in production; env-resolved URL `VITE_SATELLITE_TILE_URL` per AZ-498). The legacy OpenStreetMap / Esri tile providers are NO LONGER consumed by the main SPA as of cycle 2 / 2026-05-12. |
| | Identity provider (suite-internal — Admin API) |
**External systems**:
@@ -139,9 +139,9 @@ contract beautifully and accessibly".
| `gps-denied-desktop/`, `gps-denied-onboard/` | REST (via `/api/gps-denied-*/*`) | Outbound | GPS-Denied operations + Test Mode (SITL feed) |
| `autopilot/` | REST (via `/api/autopilot/*`) | Outbound | Aircraft autopilot configuration (admin-side) |
| `resource/` | REST (via `/api/resource/*`) | Outbound | Static resource fetch (icons, configs not bundled in the SPA) |
| OpenStreetMap tile servers | HTTPS (Leaflet TileLayer) | Outbound | Map raster tiles (browser-direct, not via nginx proxy) |
| Satellite tile provider | HTTPS (Leaflet TileLayer with env-configured URL) | Outbound | Satellite imagery (only consumed by mission-planner today) |
| OpenWeatherMap | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. **Hardcoded API key in `flightPlanUtils.ts:60` — security finding to fix at Step 4.** |
| Suite-internal `satellite-provider` service for satellite tiles | HTTPS (Leaflet TileLayer with env-configured URL `VITE_SATELLITE_TILE_URL`); same-origin in production via nginx; cookie auth (`crossOrigin="use-credentials"`) | Outbound (intra-suite) | Satellite map raster tiles. Replaces the previously-used OpenStreetMap and Esri ArcGIS World Imagery tile servers as of cycle 2 / 2026-05-12 (AZ-498) — air-gap restriction E1 satisfied without a stub. |
| OpenWeatherMap (main SPA) | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-448 / AZ-449. |
| OpenWeatherMap (mission-planner) | HTTPS (`api.openweathermap.org/data/2.5/weather`) | Outbound | Wind data for the mission-planner port. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-499; `getWeatherData(lat, lon)` returns `null` and issues NO fetch when the key is unset (fail-soft contract). The previously-committed literal `335799082893fad97fa36118b131f919` is defended against re-introduction by `STC-SEC1C` and tracked for manual OWM-dashboard revocation under AC-42. |
## 2. Technology Stack
@@ -170,7 +170,7 @@ contract beautifully and accessibly".
- **Static bundle only**: the UI ships zero server-side runtime. nginx serves `dist/` and reverse-proxies `/api/<service>/` to the matching suite service.
- **ARM-first**: production target is ARM-class edge devices; CI builds ARM64 only today (no AMD64 image in the pipeline).
- **Air-gapped friendly**: the SPA is bundled fully; only OpenWeatherMap and map tiles require internet. Field deployments will need an offline tile cache (not implemented).
- **Air-gapped friendly**: the SPA is bundled fully. As of cycle 2 / 2026-05-12 (AZ-498), map tiles are served by the suite-internal `satellite-provider` service on the same origin via nginx — restriction E1 is satisfied for tiles without a stub. The only remaining direct-from-browser external dependency is OpenWeatherMap (env-resolved per AC-42; fail-soft when the key is unset). Field deployments that go fully air-gapped MUST set `VITE_OWM_API_KEY=""` (or omit it) so `getWeatherData` returns `null` instead of attempting an external fetch.
- **No test framework**: legacy carry-over; the WPF `Azaion.Test` project tested utilities only; full test infrastructure is being built fresh under autodev.
- **Bilingual UI required**: Ukrainian + English are mandatory per the legacy WPF UX. English-only SaaS-style copy is a regression — finding tracked.
@@ -272,14 +272,14 @@ contract beautifully and accessibly".
| `08_admin/AdminPage` | `annotations/` + `admin/` + `flights/` | REST | Request-Response | `GET /api/annotations/classes` (read), `POST /api/admin/classes` (create), `DELETE /api/admin/classes/${id}` (delete — no ConfirmDialog, finding B4), `POST /api/admin/users`, `PATCH /api/admin/users/${id}` (deactivate), `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Cross-service reads** — admin page reads aircraft from `flights/` and classes from `annotations/`. |
| `09_settings/SettingsPage` | `annotations/` + `flights/` | REST | Request-Response | `GET/PUT /api/annotations/settings/system`, `GET/PUT /api/annotations/settings/directories`, `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Settings endpoints route to `annotations/`**, NOT `admin/` as initially drafted. |
| `05_flights/FlightsPage` | `flights/` | REST + SSE | Request-Response + Event | `GET /api/flights/aircrafts`, `GET /api/flights/${id}/waypoints`, **`createSSE('/api/flights/${id}/live-gps', ...)` — live-GPS SSE for aircraft telemetry**, `POST /api/flights`, `DELETE /api/flights/${id}`, `DELETE /api/flights/${id}/waypoints/${wpId}` (loop), `POST /api/flights/${id}/waypoints` (loop, lossy shape — finding #20). |
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with **hardcoded API key** — security finding. |
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with env-resolved key + base URL since AZ-448 / AZ-449 (closes the original security finding; see AC-20 + AC-42). |
### External Integrations
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|----------------|----------|------|-------------|--------------|
| OpenStreetMap tiles | HTTPS (Leaflet TileLayer) | None | OSM Tile Usage Policy | Map renders blank / stale; no fallback today |
| OpenWeatherMap | HTTPS | **Hardcoded API key in source** | Free-tier 60 calls/min | Errors silently swallowed in `flightPlanUtils.ts` (finding) — wind data missing → battery/duration estimates wrong, no UI surface |
| Suite-internal `satellite-provider` for satellite tiles | HTTPS (Leaflet TileLayer); same-origin via nginx in production; cookie auth (`crossOrigin="use-credentials"`) | HttpOnly same-origin cookie set by `admin/` | Bounded by suite ops (no external usage policy) | 401 / 503 on a tile request renders a broken-tile placeholder for the failing cell; rest of the SPA stays interactive (per NFT-RES-11). Cycle 2 / 2026-05-12 — AZ-498. |
| OpenWeatherMap | HTTPS | Env-resolved key (`VITE_OWM_API_KEY`); never hardcoded since AZ-448 / AZ-499 | Free-tier 60 calls/min | Errors silently swallowed in main SPA's `flightPlanUtils.ts` (existing finding); mission-planner `WeatherService.getWeatherData` now returns `null` and issues NO outbound fetch when the key is unset (AZ-499 fail-soft contract — AC-42). |
| Suite identity provider (admin/) | REST + HttpOnly refresh cookie | JWT bearer + refresh-token rotation | server-enforced | 401 → `ProtectedRoute` redirects to `/login`; refresh-token rotation handled inside `AuthContext` (mostly) |
## 6. Non-Functional Requirements
@@ -56,7 +56,7 @@ The two trees are intentionally disjoint at the file level (no cross-imports —
| Page composition | `flightPlanning/flightPlan.tsx`, `LeftBoard.tsx` | Canonical page shape (sidebar + map). |
| Map | `flightPlanning/MapView.tsx` (cycle-paired with `MiniMap.tsx`), `MiniMap.tsx`, `DrawControl.tsx`, `MapPoint.tsx` | Reference Leaflet integration. **Cycle**: `MiniMap` imports the *named* helper `UpdateMapCenter` from `MapView`; `MapView` imports `MiniMap` as JSX child. Document the contract precisely if porting both at once. |
| Panels | `flightPlanning/PointsList.tsx`, `AltitudeChart.tsx`, `AltitudeDialog.tsx`, `WindEffect.tsx`, `TotalDistance.tsx`, `JsonEditorDialog.tsx`, `LanguageSwitcher.tsx`, `Aircraft.ts` | Reference panel shapes. Several have richer behaviour than the current SPA siblings. |
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is currently an inferior port (silent errors, sequential `await`, hardcoded API key). |
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is still an inferior port on remaining axes (silent errors, sequential `await`). The hardcoded-API-key gap was closed by AZ-448 / AZ-449 (main SPA) and AZ-499 (mission-planner — env-resolved + fail-soft). |
| i18n | `flightPlanning/LanguageContext.tsx`, `constants/translations.ts`, `constants/languages.ts` | Local translation pattern. The port should converge to `00_foundation/i18n` instead. |
| Constants | `constants/{actionModes,maptypes,tileUrls,purposes}.ts` | Reference constant tables. |
| Icons | `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | Reference icon factory. |
@@ -8,9 +8,9 @@
| Env | How it runs | API base | Auth | Tile providers |
|-----|-------------|----------|------|----------------|
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | OSM + satellite (env-configurable in mission-planner only) |
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Same |
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same |
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | Suite-internal `satellite-provider` via env-configurable `VITE_SATELLITE_TILE_URL` (defaults to `http://localhost:5100/tiles/{z}/{x}/{y}` when unset). Cookie auth requires same-origin; running the SPA at `localhost:5173` and `satellite-provider` at `localhost:5100` cannot send the auth cookie cross-port — recommend reaching `satellite-provider` through the suite's local nginx OR running it with auth disabled in dev (per AZ-498 risk #2). `mission-planner/` keeps its own independent `VITE_SATELLITE_TILE_URL`. |
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Suite-internal `satellite-provider` on the same origin (nginx-fronted); cookie auth attached automatically. |
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same as Stage. Replaces the previously-used external OpenStreetMap and Esri tile providers as of cycle 2 / 2026-05-12 (AZ-498) — production deploy is gated on the cross-workspace satellite-provider cookie-auth ticket landing (autodev Step 16). |
## 2. Configuration model
@@ -21,20 +21,22 @@ The SPA bundle is **fully static**. No env vars are read at runtime by the bundl
| Backend API URL | nginx `proxy_pass` (`nginx.conf`) — same nginx config across stage / prod | Base URLs are intra-cluster service names (`http://annotations:8080`, etc.); the URL difference between environments is hidden by the orchestrator's DNS |
| Auth cookie domain | Set by suite admin/ service on `Set-Cookie` | UI does not control |
| Refresh-token lifetime | Set by suite admin/ service | UI tolerates any TTL |
| Tile provider URL (mission-planner) | `.env.example` declares `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
| OpenWeatherMap API key | **Hardcoded in source** (`flightPlanUtils.ts:60`) | Security finding — Step 4 fix to remove + proxy via suite |
| Satellite tile provider URL (main SPA) | `.env.example` declares `VITE_SATELLITE_TILE_URL`; resolved at build time via `getTileUrl()` (`src/features/flights/types.ts`) with `DEFAULT_SATELLITE_TILE_URL` fallback. Cycle 2 / AZ-498. |
| Satellite tile provider URL (mission-planner) | `mission-planner/.env.example` declares its own independent `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
| OpenWeatherMap API key + base URL (main SPA) | `.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; resolved by `getOwmBaseUrl()` and the `flightPlanUtils.ts` builder. Closed AZ-448 / AZ-449 (no longer hardcoded). |
| OpenWeatherMap API key + base URL (mission-planner) | `mission-planner/.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; `WeatherService.getWeatherData(lat, lon)` returns `null` and issues NO outbound `fetch` when the key is unset (fail-soft). Closed cycle 2 / AZ-499. The previously-committed literal value MUST be revoked at the OWM dashboard (manual deliverable — AC-42 / AZ-499 AC-7); `STC-SEC1C` defends against re-introduction. |
| `AZAION_REVISION` | Stamped into image at build time | For diagnostics |
## 3. Why no `.env`
## 3. `.env` strategy
The workspace `.env.example` is **absent** today. The `README.md` "Local development" section explicitly notes this as a Step 4 testability fix.
Step 4 testability + cycle 2 added a workspace `.env.example` (resolved by Vite at build time via `import.meta.env.VITE_*`). Today it declares: `VITE_OWM_API_KEY`, `VITE_OWM_BASE_URL` (AZ-448 / AZ-449), and `VITE_SATELLITE_TILE_URL` (AZ-498). `mission-planner/.env.example` mirrors the OWM pair (AZ-499) and keeps its own independent `VITE_SATELLITE_TILE_URL`.
**Trade-off**: avoiding a build-time env injection means `dist/` is identical across environments, which is great for promotability (the same image flows dev → stage → prod). The cost: the OpenWeatherMap key (and any future runtime config) cannot be changed without a rebuild.
**Trade-off**: Vite resolves `import.meta.env.VITE_*` at build time, so `dist/` is environment-specific once a non-empty `VITE_OWM_API_KEY` is baked in — the OpenWeatherMap key (and any future build-time config) cannot be changed without a rebuild. This trades promotability for the air-gap-friendly pattern that lets a deploy ship with `VITE_OWM_API_KEY=""` (no OWM call, fail-soft `null` return) when the deployment must not touch the internet.
**Future direction** (Step 4 / Step 5):
- Move the OpenWeatherMap call server-side (`flights/` service) — eliminates the bundled key entirely.
- Introduce a runtime `/config.json` that nginx serves — lets ops change feature flags / tile URLs without rebuilding.
- OR keep the static bundle and use Vite's `define` for build-time injection of safe-to-publish values (no secrets).
**Future direction** (still open):
- Move the OpenWeatherMap call server-side (`flights/` service) — would eliminate the bundled key entirely; the env-var hardening in cycle 2 reduces the urgency but does not remove the option.
- Introduce a runtime `/config.json` that nginx serves — would let ops change feature flags / tile URLs without rebuilding.
- OR keep the static bundle and continue using Vite's `import.meta.env` for build-time injection of safe-to-publish values (current approach).
## 4. Promotability
@@ -48,4 +50,4 @@ In practice: branch separation is the gating mechanism. Once dev → stage → m
- **`bun.lock`**: committed (per `package.json`'s `packageManager` field). `package-lock.json` is gitignored.
- **`.idea/`, `.claude/`, `.superpowers/`**: gitignored — IDE / agent metadata.
- **Playwright entries in `.gitignore`**: present but aspirational — Playwright is not installed (Step 57 territory).
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL` and runs as a sibling Vite app. Not bundled into the deployed image.
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL` and (cycle 2 / AZ-499) `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`. Runs as a sibling Vite app; not bundled into the deployed image (per AC-31 / NFT-RES-LIM-04).
+65
View File
@@ -0,0 +1,65 @@
# Documentation Ripple Log — Cycle 2 (Phase B)
> Generated during Step 13 (Update Docs) of the autodev existing-code flow, cycle 2.
> Task specs in scope: `AZ-498_satellite_tile_swap.md`, `AZ-499_mission_planner_weather_env.md` (both in `_docs/02_tasks/done/`).
> Implementation: single batch (`_docs/03_implementation/batch_11_report.md`).
## Scope analysis (Task Step 0)
Direct source files changed by Cycle 2 batch 11:
| Source file | Changed in | Touched module / component / system doc |
|---|---|---|
| `src/features/flights/types.ts` | AZ-498 (replaced `TILE_URLS` with `getTileUrl()` + `DEFAULT_SATELLITE_TILE_URL`) | `modules/src__features__flights.md` (updated by implementer at batch-11 commit time) |
| `src/features/flights/FlightMap.tsx` | AZ-498 (drop `mapType` state + toggle button + `MiniMap mapType` prop; single `<TileLayer crossOrigin="use-credentials">`) | same group doc as above |
| `src/features/flights/MiniMap.tsx` | AZ-498 (drop `mapType` prop) | same group doc |
| `src/vite-env.d.ts` | AZ-498 (replaced `VITE_OSM_TILE_URL` / `VITE_ESRI_TILE_URL` with `VITE_SATELLITE_TILE_URL`) | covered in `modules/src__features__flights.md` Tile URL section + `deployment/environment_strategy.md` (this run) |
| `.env.example` | AZ-498 | `deployment/environment_strategy.md` §2 (this run) |
| `src/i18n/en.json`, `src/i18n/ua.json` | AZ-498 (removed `flights.planner.satellite` key in lockstep — STC-FP22 parity preserved) | no module doc change needed (i18n parity is enforced by static check, not described in module docs) |
| `mission-planner/src/services/WeatherService.ts` | AZ-499 (env vars + fail-soft `null` when key unset) | `modules/mission-planner.md` (updated by implementer at batch-11 commit time) |
| `mission-planner/.env.example` | AZ-499 | same group doc + `deployment/environment_strategy.md` (this run) |
| `mission-planner/src/vite-env.d.ts` | AZ-499 | same group doc |
| `tests/security/banned-deps.json` | AZ-499 (added `owm_key_in_source` kind) | `tests/security-tests.md` NFT-SEC-09 step 3 (Step 12 cycle-update) |
| `scripts/check-banned-deps.mjs` | AZ-499 (extended source-tree dispatch) | static-check infrastructure — covered by AZ-482 module doc (no new entry needed; same dispatch shape) |
| `scripts/run-tests.sh` | AZ-499 (added `STC-SEC1C` row) | `tests/environment.md` Test Execution + `tests/security-tests.md` NFT-SEC-09 (Step 12) |
| `e2e/docker-compose.suite-e2e.yml` | AZ-498 (replaced dead `VITE_TILE_BASE_URL` with `VITE_SATELLITE_TILE_URL`) | `tests/environment.md` (Step 12) |
| `e2e/stubs/tile/server.ts` | AZ-498 (rewrote `classify()` for `/tiles/{z}/{x}/{y}` shape) | `tests/environment.md` (Step 12) |
| `e2e/tests/infrastructure.e2e.ts` | AZ-498 (AC-2 rewritten; OSM removed from `EXTERNAL_HOSTS`) | `tests/blackbox-tests.md` FT-P-59 (Step 12) |
| `tests/msw/handlers/tiles.ts` | AZ-498 (rewrote handlers from OSM/Esri `.png` to `/tiles/{z}/{x}/{y}` with cookie-auth headers) | covered by FT-P-57 / FT-P-59 (Step 12) |
System-level docs (`architecture.md`, `system-flows.md`, `deployment/environment_strategy.md`): **architecture.md + environment_strategy.md TOUCHED this run**; `system-flows.md` not touched (no flow diagrams referenced map tiles or OWM). The architectural changes are: external-integration table (OSM/Esri removed from outbound; suite-internal `satellite-provider` added), system-boundaries table (tile providers row updated), § 5 External Integrations (failure-mode column updated for satellite tiles + OWM), Air-gap section in § 2 (tiles no longer external; OWM remains external but env-resolved + fail-soft).
Problem-level docs: **acceptance_criteria.md TOUCHED this run** — added AC-41 (self-hosted satellite tiles + cookie auth) and AC-42 (mission-planner OWM env hardening + STC-SEC1C); updated AC-20 row to reference the closure tasks; updated Coverage status section to move AC-20 from "Currently violated" to "Currently met & enforced" and add AC-41 / AC-42 there as well. `restrictions.md` not touched (the air-gap restriction E1 is now better satisfied for tiles, but the restriction text itself does not change).
Contract docs: `_docs/02_document/contracts/satellite-provider/tiles.md` was drafted in Step 9 (New Task) and updated by the implementer to reference AZ-498 in the `Consumer tasks` field — no further edit this run.
## Import-graph ripple (Task Step 0.5)
The reverse-dependency set of the changed files is small and is **already captured in the direct list above** plus the test-spec / system-level updates from this run. Specifically:
- `src/features/flights/types.ts` exports `getTileUrl()` + `DEFAULT_SATELLITE_TILE_URL` (cycle 2) plus the existing waypoint / mission JSON shapes. Importers: `FlightMap.tsx`, `MiniMap.tsx` (both directly in scope), and the new fast test `src/features/flights/__tests__/satellite_tile.test.tsx`. No additional consumer needs a doc refresh — `FlightsPage.tsx` consumes `FlightMap` / `MiniMap` as JSX components without referencing the tile URL plumbing.
- `src/features/flights/FlightMap.tsx` is imported by `FlightsPage.tsx` (which composes the page); the public prop surface of `FlightMap` is unchanged on tile-related axes (no exported tile constants, no `mapType` exposure to callers). FlightsPage's module-doc section (`modules/src__features__flights.md`) already reflects the change because the implementer updated the group doc at batch-11 commit time.
- `src/features/flights/MiniMap.tsx` lost a public prop (`mapType`) — this IS a public surface change. Callers: only `FlightMap.tsx` (intra-component); no external caller. The change was applied in lockstep in the same batch, so there is no "stale caller" to chase.
- `mission-planner/src/services/WeatherService.ts` keeps its public `getWeatherData(lat, lon)` signature; only the internal env-var resolution + fail-soft branch changed. Callers in `mission-planner/` (page-level components in the legacy port-source) see no behavior change beyond `null` returned when the key is unset — already documented under `modules/mission-planner.md` Migration Notes.
Therefore: **no additional doc was added to the refresh set by ripple analysis** beyond the system-level docs already updated for cycle-wide concerns (architecture.md external integrations + environment_strategy.md env-var matrix).
## Tooling notes
- Ripple analysis was performed by reading the implementer's `_docs/03_implementation/batch_11_report.md` (which enumerates every modified file with rationale), then cross-checking each changed file's importers via `Grep` against `src/features/flights/` and `mission-planner/`. The repo has no `madge` / `depcruise` configured; this counts as the "directory-proximity + manual import inspection" fallback path from `document/workflows/task.md` Task Step 0.5 #6 — full coverage was achievable because the changed file set is small and bounded by two well-known package roots (`src/features/flights/` and `mission-planner/src/services/`).
- No static analyzer was used to discover indirect importers. None was needed: the public-surface changes are minimal (one prop drop on `MiniMap`, one preserved-signature env-resolution change on `getWeatherData`, one new function on `types.ts` replacing a removed const), and all in-tree callers were updated in the same batch.
## Outcome
Cycle-2 documentation refresh complete. Updated this run:
| Level | Doc | Reason |
|---|---|---|
| System-level | `_docs/02_document/architecture.md` | Removed stale OSM/Esri tile entries; added suite-internal `satellite-provider` row; updated External Integrations failure-mode for tiles + OWM; corrected stale "hardcoded API key" claim. |
| System-level | `_docs/02_document/deployment/environment_strategy.md` | Added env-var matrix rows for `VITE_SATELLITE_TILE_URL` (main SPA + mission-planner) and `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` (main SPA + mission-planner); updated tile-providers column for all three envs; updated `.env` strategy section to reflect cycle-2 reality. |
| Component | `_docs/02_document/components/05_flights/description.md` | Removed stale "hardcoded API key" claim from the legacy mission-planner port-source comparison (line 59). |
| Problem | `_docs/00_problem/acceptance_criteria.md` | Added AC-41 (satellite tiles + cookie auth + toggle removal) and AC-42 (mission-planner OWM env hardening + STC-SEC1C); reworded AC-20; updated Coverage status. |
Module-level docs (`modules/src__features__flights.md`, `modules/mission-planner.md`) and the contract doc (`contracts/satellite-provider/tiles.md`) were already updated by the implementer at batch-11 commit time and verified consistent with the source tree at the start of this run; no additional change applied.
Test-spec docs (`tests/blackbox-tests.md`, `tests/security-tests.md`, `tests/resilience-tests.md`, `tests/environment.md`, `tests/traceability-matrix.md`) were updated in the preceding Step 12 (Test-Spec Sync) cycle-update — see the Step 12 commit for those changes.
+179
View File
@@ -1470,6 +1470,185 @@ Every test is observed at the SPA's public surface — DOM, ARIA, outbound netwo
---
### FT-N-16: mission-planner `getWeatherData` fail-soft when `VITE_OWM_API_KEY` is unset
**Traces to**: AC-42 (AZ-499 AC-3)
**Profile**: fast
**Input data**: build-time env with `VITE_OWM_API_KEY=""` (or undefined).
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` | spy installed, no calls yet |
| 2 | Stub `import.meta.env.VITE_OWM_API_KEY = ""` and invoke `getWeatherData(50, 30)` | resolves |
| 3 | Inspect return value | `=== null` |
| 4 | Inspect fetch spy | `mock.calls.length === 0` |
**Pass criteria**: function returns `null` AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
**Max execution time**: 1s (env stub + sync inspection only).
**Expected result source**: AZ-499 AC-3 (no `results_report.md` row needed — behavioral test, no input data).
---
## Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
The scenarios below were appended via `/test-spec` cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to `traceability-matrix.md`; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per `_docs/03_implementation/batch_11_report.md`; AZ-499 AC-7 is a manual deliverable, not a test).
### FT-P-56: Self-hosted satellite tile URL is env-var resolved
**Traces to**: AC-41 (AZ-498 AC-1, AC-2)
**Profile**: fast
**Input data**: build-time env with `VITE_SATELLITE_TILE_URL` set, unset, or set with a trailing slash.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` and call `getTileUrl()` | returns the env value verbatim |
| 2 | Stub `VITE_SATELLITE_TILE_URL=""` and call `getTileUrl()` | returns `DEFAULT_SATELLITE_TILE_URL` (`http://localhost:5100/tiles/{z}/{x}/{y}`) |
| 3 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/` (trailing slash) | returns the value with the trailing slash stripped |
| 4 | Mount `<FlightMap>` with the env unset; inspect rendered `<TileLayer>` `data-tile-url` | equals `DEFAULT_SATELLITE_TILE_URL` |
**Pass criteria**: all four assertions hold. Mirrors the established `getOwmBaseUrl()` / `getApiBase()` env-resolution pattern.
**Max execution time**: 2s (jsdom render + four stub variations).
**Expected result source**: AZ-498 AC-1, AC-2 (no `results_report.md` row needed — env-var plumbing, no input data fixture).
---
### FT-P-57: `<TileLayer crossOrigin="use-credentials">` enables cookie-auth on tile fetches
**Traces to**: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth
**Profile**: fast
**Input data**: `<FlightMap>` and `<MiniMap>` mounted with the default tile URL.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Mount `<FlightMap>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
| 2 | Mount `<MiniMap pointPosition={…}>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
| 3 | (e2e — gated) Issue `GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0>` from the rendered map; inspect outbound request | `request.credentials === "include"` (browser attaches the same-origin auth cookie) |
**Pass criteria**: every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
**Max execution time**: 2s for steps 1+2 (fast); e2e step is part of `infrastructure.e2e.ts` — bounded by suite-e2e timeout.
**Expected result source**: AZ-498 AC-3 (no `results_report.md` row — DOM-attribute observable).
---
### FT-P-58: Classic/satellite map toggle, `mapType` state, and `MiniMap.Props.mapType` are removed
**Traces to**: AC-41 (AZ-498 AC-4)
**Profile**: fast
**Input data**: `<FlightMap>` mounted with the default tile URL; `<MiniMap>` mounted with only `pointPosition` (no `mapType` prop).
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Mount `<FlightMap>`; query `screen.queryByRole('button', { name: /satellite|classic/i })` | returns `null` |
| 2 | Mount `<FlightMap>`; query `screen.getAllByTestId('tile-layer')` | length `=== 1` (no per-mode branching, single layer) |
| 3 | Compile-time check: instantiate `<MiniMap pointPosition={…}>` without `mapType` | TypeScript `tsc --noEmit -p tsconfig.test.json` succeeds (STC-T1) |
| 4 | Compile-time check: source-tree grep for any remaining `mapType` reference under `src/features/flights/` | zero hits (compilation error if not — covered by STC-T1) |
**Pass criteria**: no toggle button, no `mapType` state, `MiniMap.Props` has no `mapType`. Removal is permanent; the `flights.planner.satellite` i18n key was removed from both `en.json` and `ua.json` in lockstep (i18n key parity preserved via STC-FP22).
**Max execution time**: 2s (jsdom render + grep).
**Expected result source**: AZ-498 AC-4.
---
### FT-P-59: e2e harness exercises the new `/tiles/{z}/{x}/{y}` path
**Traces to**: AC-41 (AZ-498 AC-6); E1 (air-gap)
**Profile**: e2e
**Input data**: suite-e2e compose stack up; `tile-stub` configured at `http://tile-stub:8082/tiles/{z}/{x}/{y}`.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | `infrastructure.e2e.ts` AC-2 — issue `GET http://tile-stub:8082/tiles/1/0/0` from the playwright runner | HTTP 200, response is a 256×256 image (JPEG) |
| 2 | Inspect response headers | `Content-Type: image/jpeg`, `Cache-Control` present, `ETag` present |
| 3 | Inspect outbound request from the SPA's `<TileLayer>` | URL matches `^http://tile-stub:8082/tiles/\d+/\d+/\d+$` (NOT `/{z}/{x}/{y}.png`, NOT the legacy `/sat/...` Esri shape) |
| 4 | Inspect `EXTERNAL_HOSTS` route guard | OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
**Pass criteria**: tile fetch shape matches the satellite-provider contract documented at `_docs/02_document/contracts/satellite-provider/tiles.md`. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the `tile-stub` which accepts requests without a cookie.
**Max execution time**: bounded by suite-e2e infrastructure-test timeout (per `e2e/tests/infrastructure.e2e.ts`).
**Expected result source**: contract at `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0; AZ-498 AC-6.
---
### FT-P-60: mission-planner `getWeatherData` uses env-resolved key + base URL
**Traces to**: AC-42 (AZ-499 AC-1, AC-2, AC-4)
**Profile**: fast
**Input data**: build-time env with `VITE_OWM_API_KEY` set + `VITE_OWM_BASE_URL` either set, unset, or set with a trailing slash.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ wind: { speed: 5, deg: 90 } }` | spy installed |
| 2 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL contains `appid=abc123` AND `units=metric` |
| 3 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` |
| 4 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5/` (trailing slash); invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` (slash stripped) |
| 5 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://api.openweathermap.org/data/2.5/weather?` (default base) |
| 6 | Inspect return value on a successful fetch | `=== { windSpeed: 5, windAngle: 90 }` (existing parsed-wind shape preserved) |
**Pass criteria**: every outbound URL is reconstructed from env vars; the public `getWeatherData(lat, lon)` signature and `WeatherData` return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
**Max execution time**: 2s (env stubs + fetch-spy assertions; no real network).
**Expected result source**: AZ-499 AC-1, AC-2, AC-4 (no `results_report.md` row — env-var plumbing).
---
### FT-P-61: mission-planner `geocodeAddress` uses env-resolved Google API key
**Traces to**: AC-43 (AZ-501 AC-1)
**Profile**: fast
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` set to a placeholder string.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ status: 'OK', results: [{ geometry: { location: { lat, lng } } }] }` | spy installed |
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz`; invoke `geocodeAddress('Kyiv, Ukraine')` | outbound URL contains `key=env-key-xyz` AND `address=Kyiv%2C%20Ukraine` |
| 3 | Inspect return value | `=== { lat, lng }` from the mocked response |
**Pass criteria**: the outbound URL is reconstructed from the env var; no literal key remains in `mission-planner/src/services/GeocodeService.ts` (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
**Max execution time**: 2s.
**Expected result source**: AZ-501 AC-1.
---
### FT-N-17: mission-planner `geocodeAddress` fail-soft when `VITE_GOOGLE_GEOCODE_KEY` is unset
**Traces to**: AC-43 (AZ-501 AC-3)
**Profile**: fast
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` empty / undefined.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch`; spy `console.warn` | spies installed |
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=''`; invoke `geocodeAddress('anywhere')` | returns `null`; fetch is NOT called; `console.warn` called exactly once with a message containing `VITE_GOOGLE_GEOCODE_KEY` |
| 3 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz` and force `fetch` to reject with `Error('boom')`; invoke `geocodeAddress('anywhere')` | returns `null`; promise does NOT throw |
**Pass criteria**: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable".
**Max execution time**: 2s.
**Expected result source**: AZ-501 AC-3.
---
## Notes carried into Phase 3
- All tests tagged `quarantined` correspond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept.
+5 -5
View File
@@ -35,14 +35,14 @@ The Azaion UI image carries no DB. The "Docker environment" is the test-time cho
| `detect` | Suite `detect/` image | Sync image detect (and future async video detect F7) | per suite compose |
| `gps-denied-desktop`, `gps-denied-onboard`, `autopilot`, `resource`, `loader` | Suite microservice images | Auxiliary services hit by the SPA (only `loader/` and `resource/` are hit on production paths today; `gps-denied-*` is target-only F12) | per suite compose |
| `owm-stub` | Tiny HTTP server returning canned OpenWeatherMap responses | Replace direct OWM HTTPS (E10) so tests are deterministic and rate-limit-free | `8081` |
| `tile-stub` | Tiny HTTP server returning a 256x256 PNG | Replace OSM tile servers | `8082` |
| `tile-stub` | Tiny HTTP server serving `GET /tiles/{z}/{x}/{y}` → 256x256 JPEG with `Content-Type: image/jpeg`, `Cache-Control`, and `ETag` headers (mirrors the satellite-provider contract at `_docs/02_document/contracts/satellite-provider/tiles.md`) | Replace the suite's `satellite-provider` tile endpoint in the e2e profile (since cycle 2 / AZ-498). The stub does NOT enforce cookie auth — the same-origin cookie path is exercised once the cross-workspace satellite-provider cookie-auth ticket lands and tile traffic flows through the real service. | `8082` |
| `test-db` | Suite-managed (Postgres per suite default) | Backs `admin/`, `flights/`, `annotations/` | Internal |
### Networks
| Network | Services | Purpose |
|---------|----------|---------|
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (OWM + tile stubs replace the only external hops). |
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (`owm-stub` + `tile-stub` replace the only external hops — OWM HTTPS, and since cycle 2 / AZ-498 the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint stands in for the previously-used external OSM/Esri tile servers). |
### Volumes
@@ -92,7 +92,7 @@ services:
environment:
BASE_URL: http://azaion-ui:80
OWM_BASE_URL: http://owm-stub:8081
TILE_BASE_URL: http://tile-stub:8082
VITE_SATELLITE_TILE_URL: "http://tile-stub:8082/tiles/{z}/{x}/{y}"
```
The compose file is part of the test-spec output; its concrete shape lands when the Decompose Tests step picks the runner (Step 5).
@@ -129,7 +129,7 @@ The compose file is part of the test-spec output; its concrete shape lands when
| Suite SSE | HTTPS | `/api/flights/<id>/live-gps`, `/api/annotations/annotations/events`, `/api/detect/stream/<jobId>` (F7 target) | bearer in `?token=` per ADR-008 |
| Bundle / image inspection | filesystem / `docker inspect` | n/a | n/a |
| OpenWeatherMap | HTTPS via `owm-stub` | per stub | none |
| OSM tiles | HTTPS via `tile-stub` | per stub | none |
| Satellite tiles | HTTPS via `tile-stub` (replacing the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint in the e2e profile) | per stub at `/tiles/{z}/{x}/{y}` | none in stub; production uses an HttpOnly same-origin cookie set by `admin/` (see `crossOrigin="use-credentials"` on every `<TileLayer>` per cycle 2 / AZ-498) |
### What the consumer does NOT have access to
@@ -192,7 +192,7 @@ Conclusion: classify as **Not hardware-dependent**. Docker headless Chromium rep
3. **Compose up**: `docker compose -f e2e/docker-compose.suite-e2e.yml up -d` — brings up `azaion-ui`, `admin`, `flights`, `annotations`, `detect`, the auxiliary services, `owm-stub`, `tile-stub`, `test-db`, and the `playwright-runner`.
4. **Run tests**: `docker compose -f e2e/docker-compose.suite-e2e.yml run --rm playwright-runner` — the runner image entrypoint is `bun run test:e2e`. Reports land in `./test-output/`.
5. **Tear down**: `docker compose -f e2e/docker-compose.suite-e2e.yml down -v` (volumes wiped between runs).
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `TILE_BASE_URL=http://tile-stub:8082`, `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `VITE_SATELLITE_TILE_URL=http://tile-stub:8082/tiles/{z}/{x}/{y}` (since cycle 2 / AZ-498 — was `TILE_BASE_URL=http://tile-stub:8082`), `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
#### Local mode (for `fast` profile + developer-machine `e2e` runs)
@@ -242,3 +242,35 @@ Failure / recovery scenarios at the SPA's observable boundary: bearer expiry, re
**Pass criteria**: row 97 — connection-lost indicator OR reconnect attempt within 10 s; stale data NOT rendered as live; reconnect attempts ≤ 1 in the 10 s window.
**Expected result source**: `results_report.md` row 97.
---
### NFT-RES-11: Tile endpoint 401/503 does NOT crash the map
**Summary**: When the `satellite-provider /tiles/{z}/{x}/{y}` endpoint returns 401 (cookie-auth failure) or 503 (Google Maps upstream down), the SPA renders a broken-tile placeholder for the failing tile(s) and the rest of the application keeps working. No React error boundary fires; no full-page crash.
**Traces to**: AC-41 (AZ-498 NFR-Reliability)
**Preconditions**:
- `<FlightMap>` mounted with a valid `VITE_SATELLITE_TILE_URL`.
- Tile endpoint configured to return 401 (auth failure) OR 503 (upstream provider down) for one or more tile coordinates.
**Fault injection**:
- (auth-failure variant) Strip / invalidate the satellite-provider auth cookie before the SPA attempts a tile fetch; tile endpoint responds 401.
- (upstream-down variant) Configure the test stub to return 503 for `GET /tiles/{z}/{x}/{y}`.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Mount `<FlightMap>`; trigger a tile load that fails per the fault | Leaflet emits a `tileerror` event for the affected coordinate |
| 2 | Observe the rendered map | broken-tile placeholder shown in the failing cell; surrounding tiles continue rendering normally |
| 3 | Observe the rest of the SPA (header, side panels, navigation) | remains interactive; no React error boundary fires; no console error of category `Uncaught` |
| 4 | Observe a recovery path (auth restored OR upstream back) | next pan/zoom successfully fetches the tile; the placeholder is replaced with the imagery |
**Pass criteria**:
- 401 response on a tile request MUST NOT crash the map; broken-tile placeholder rendered in the failing cell, rest of SPA interactive.
- 503 response treated identically to 404/transient failure (fault budget — recovery path works after the upstream returns).
- No new uncaught error in the console attributable to the failed tile.
**Expected result source**: AZ-498 NFR-Reliability (no `results_report.md` row needed — observable through DOM state and console).
**Note on follow-up**: AZ-498 risk #5 flags an optional `tileerror` listener on `<MapContainer>` that surfaces a structured warning + an optional inline banner ("Imagery unavailable; please re-sign-in"). If/when that lands, this scenario gains a Step 5 asserting the banner appears within 2 s of the first tile error.
+28 -7
View File
@@ -145,20 +145,41 @@ Blackbox security assertions against the SPA's observable surface: token storage
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
**Traces to**: AC-20, P10
**Traces to**: AC-20, AC-42 (AZ-499 AC-5, AC-7), P10
**Profile**: static (source) + static (bundle)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Regex sweep `src/` and `mission-planner/src/` for the literal current OWM key value | `match_count == 0` (row 63) |
| 2 | Regex sweep for `appid=` and `api_key=` literal occurrences in source URLs | `match_count == 0` (row 63) |
| 3 | Scan `dist/**/*.js` post-build for the literal key | `match_count == 0` (Phase 3 may downgrade to "until Step 4 fix") |
| 1 | `STC-SEC1` Regex sweep `src/` for `appid=[a-zA-Z0-9]{6,}` (filtered to exclude `import.meta.env` / `process.env` references) | `match_count == 0` (row 63) |
| 2 | `STC-SEC1B` — Scan `dist/**/*.js` post-build for the literal key value | `match_count == 0` (NFT-SEC-09 AC-1 dist portion) |
| 3 | `STC-SEC1C` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed key (`335799082893fad97fa36118b131f919`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` | `match_count == 0` (row 63 — AZ-499 AC-5) |
**Pass criteria**: row 63.
**Status**: `quarantined` for source check until Step 4 fix; the bundle-scan check passes immediately for `src/` (mission-planner not bundled, AC-31).
**Expected result source**: `results_report.md` row 63.
**Pass criteria**: row 63 (project-level AC-20) AND AZ-499 AC-5 (source scan must reject any future re-introduction of the literal key under `src/` or `mission-planner/`).
**Status**: All three checks ACTIVE (no quarantine). The source check was un-quarantined on cycle 2 close (2026-05-12) when AZ-499 (a) replaced the hardcoded key in `mission-planner/src/services/WeatherService.ts` with `import.meta.env.VITE_OWM_API_KEY` and (b) added `STC-SEC1C` so a regression cannot silently re-introduce the literal across either source tree (closing the AZ-482 source-scan gap that previously only checked `src/` for the regex shape and `dist/` for the literal — `mission-planner/` stays out of `dist/` per STC-S5, so the dist scan alone could not catch it).
**Defense-in-depth note**: the previously-committed key value (`335799082893fad97fa36118b131f919`) MUST be revoked at the OpenWeatherMap dashboard — this is AZ-499 AC-7, a manual deliverable, not a test. STC-SEC1C complements but does not replace key revocation.
**Expected result source**: `results_report.md` row 63; AZ-499 AC-5.
---
### NFT-SEC-09b: Google Geocode API key is not shipped in source
**Traces to**: AC-43 (AZ-501 AC-1, AC-4, AC-6)
**Profile**: static (source) + fast (env-resolution + fail-soft contract)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | `STC-SEC1D` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed Google key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=google_key_in_source` | `match_count == 0` (AZ-501 AC-4) |
| 2 | Fast: import `mission-planner/src/services/GeocodeService.ts` and stub `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`; assert outgoing fetch URL contains the env-resolved key | URL contains `key=<env-value>` (AZ-501 AC-1; `tests/mission_planner_geocode.test.ts`) |
| 3 | Fast: stub `VITE_GOOGLE_GEOCODE_KEY=''` and call `geocodeAddress('Kyiv')` | returns `null`, no fetch issued, single `console.warn` mentioning `VITE_GOOGLE_GEOCODE_KEY` (AZ-501 AC-3) |
**Pass criteria**: AZ-501 AC-1, AC-3, AC-4 — env-resolved + fail-soft + static gate against literal re-introduction.
**Status**: ACTIVE on cycle 2 close (2026-05-12). The key was extracted from `mission-planner/src/config.ts` to a new `services/GeocodeService.ts` module to enable isolated env-resolution + fail-soft testing (mirrors AZ-499 / WeatherService pattern).
**Defense-in-depth note**: the previously-committed key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`) MUST be revoked at the Google Cloud Console — this is AZ-501 AC-6, a manual deliverable, not a test. STC-SEC1D complements but does not replace key revocation.
**Expected result source**: AZ-501 AC-1, AC-3, AC-4.
---
@@ -25,7 +25,7 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| AC-17 | ProtectedRoute spinner a11y + timeout | FT-P-32, FT-P-33 [Q], NFT-RES-04 [Q] | 58, 59 | Covered (quarantined for timeout) |
| AC-18 | Browser support — Chromium + Firefox latest 2 | FT-P-34, NFT-PERF-10 | 60, 98 | Covered (manual smoke, no automated gate today) |
| AC-19 | Mobile / desktop breakpoint variants | FT-P-35, FT-P-36 | 61, 62 | Covered |
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 [Q for source until Step 4] | 63 | Covered (quarantined for source check) |
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 (all 3 steps active — source check un-quarantined on cycle 2 / 2026-05-12 by AZ-499) | 63 | Covered |
| AC-21 | UserSettings panel-width persistence | FT-P-37 [Q], FT-P-38 [Q], NFT-PERF-08 [Q] | 64, 65 | Covered (quarantined) |
| AC-22 | RBAC client-side route gates | FT-N-03 [Q], FT-N-04, FT-N-05 [Q], NFT-SEC-05 [Q], NFT-SEC-06 [Q], NFT-RES-08 | 08, 09, 10 | Covered (quarantined for `/admin` + `/settings` gates) |
| AC-23 | Auth refresh transparency | FT-P-02, FT-P-03, NFT-PERF-02, NFT-RES-01 | 11, 12 | Covered |
@@ -51,6 +51,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| AC-N3 | No offline mode | NFT-RES-03, NFT-SEC-12 | 93 | Covered |
| AC-N4 | No response-signature library | NFT-SEC-11 | 94 | Covered |
| AC-N5 | Dropped legacy features (Sound Detections, Drone Maintenance) | NFT-SEC-13 | 95 | Covered |
| AC-41 | Map tiles served by self-hosted `satellite-provider` via cookie auth; classic/satellite toggle removed (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-498) | FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11; STC-T1 (env-decl typecheck), STC-FP22 (i18n parity post-key removal), STC-ARCH-01 + STC-ARCH-02 (architecture gates stay green) | n/a — env-var plumbing + DOM observable + e2e contract; no `results_report.md` row required | Covered |
| AC-42 | mission-planner OpenWeatherMap key + base URL externalized via Vite env vars; fail-soft on missing key; STC-SEC1C source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-499) | FT-P-60, FT-N-16; NFT-SEC-09 step 3 (STC-SEC1C); STC-T1 (env-decl typecheck) | 63 (literal-key scan shares row 63 with AC-20) | Covered (manual deliverable AZ-499 AC-7 — old key revocation at OWM dashboard — tracked separately, not a test) |
| AC-43 | mission-planner Google Geocode API key extracted to a new `services/GeocodeService.ts` module + externalized via Vite env var; fail-soft + console.warn on missing key; STC-SEC1D source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12 from security audit `_docs/05_security/`, ticket AZ-501) | FT-P-61, FT-N-17; NFT-SEC-09b (STC-SEC1D); STC-T1 (env-decl typecheck) | n/a — env-var plumbing + console-warn assertion; no `results_report.md` row required | Covered (manual deliverable AZ-501 AC-6 — old key revocation at Google Cloud Console — tracked separately, not a test) |
| AC-44 | Vite + PostCSS upgraded past CVE-2026-39363 / GHSA-p9ff-h696-f583 / GHSA-4w7w-66w2-5vf9 / GHSA-qx2v-qp2m-jg93 in both roots via `package.json` `overrides` flooring transitive resolutions to safe versions (added cycle 2 / 2026-05-12 from security audit, ticket AZ-502) | `bun audit` (zero advisories in both roots after `bun install`) | n/a — supply-chain hygiene; verified by audit tool exit code | Covered (CI gate `bun audit --severity high` in `.woodpecker/build-arm.yml` is a Phase B follow-up — see `_docs/05_security/infrastructure_review.md` F-INF-1) |
## Restrictions Coverage
@@ -104,10 +108,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| Category | Total Items | Covered | Partially Covered | Not Covered | N/A (meta) | Coverage % (Covered+Partial) |
|----------|-------------|---------|-------------------|-------------|-----------|--------------------|
| Acceptance Criteria | 40 | 40 | 0 | 0 | 0 | 100% (24 fully ungated, 16 with Phase 3 quarantine markers) |
| Acceptance Criteria | 44 | 44 | 0 | 0 | 0 | 100% (cycle-2 deltas: AC-41, AC-42, AC-43, AC-44 added; AC-20 source check no longer quarantined) |
| Anti-Criteria | 5 | 5 | 0 | 0 | 0 | 100% |
| Restrictions | 41 | 17 | 8 | 13 | 3 | 61% |
| **Total** | **86** | **62** | **8** | **13** | **3** | **81%** |
| **Total** | **90** | **66** | **8** | **13** | **3** | **82%** |
Acceptance criterion coverage exceeds the 75 % template threshold. Restriction coverage is short of 75 % because most of the un-covered restrictions are dependency-version pins (S1-S11) for which a single static check pass (planned `STC-S*` family) would lift them to Covered without changing the SPA's observable behavior.
@@ -128,7 +132,7 @@ Acceptance criterion coverage exceeds the 75 % template threshold. Restriction c
## Quarantine List (running)
The following 18 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition.
The following 17 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition. (Cycle 2 / 2026-05-12 update: NFT-SEC-09 source check REMOVED from this list — closed by AZ-499 + STC-SEC1C; new AC-41 / AC-42 tests added in this cycle are NOT quarantined.)
| Test | Reason | Activates when |
|------|--------|---------------|
@@ -144,7 +148,6 @@ The following 18 tests assert against a Phase B target or a Step 4 fix and are q
| NFT-PERF-03 / NFT-RES-02 | SSE refresh-rotation reconnect missing | Step 8 hardening |
| NFT-PERF-08 / NFT-PERF-09 | Tied to FT-P-37 / FT-N-13 quarantines | per above |
| NFT-SEC-05, NFT-SEC-06 | Tied to FT-N-03, FT-N-05 | per above |
| NFT-SEC-09 (source check) | OpenWeatherMap key still in source today | Step 4 fix |
| NFT-RES-04 | Tied to FT-P-33 | per above |
## Phase 3 (Data Validation Gate) — Open Items to Resolve